refactor home
This commit is contained in:
+3
-1
@@ -35,7 +35,7 @@ export const fa = {
|
|||||||
factureCount: "پیش فاکتور تایید نشده",
|
factureCount: "پیش فاکتور تایید نشده",
|
||||||
orderCount: "سفارش های در حال انجام",
|
orderCount: "سفارش های در حال انجام",
|
||||||
orderDoneCount: "سفارشات انجام شده",
|
orderDoneCount: "سفارشات انجام شده",
|
||||||
services: "خدمات",
|
products: "محصولات",
|
||||||
submitNewOrder: "درخواست خودرا ثبت کنید",
|
submitNewOrder: "درخواست خودرا ثبت کنید",
|
||||||
submitNewOrderButton: "درخواست جدید",
|
submitNewOrderButton: "درخواست جدید",
|
||||||
specialSolution: "راهحل اختصاصی برای شما در نگاره!",
|
specialSolution: "راهحل اختصاصی برای شما در نگاره!",
|
||||||
@@ -47,6 +47,8 @@ export const fa = {
|
|||||||
freeConsultation: "دریافت مشاوره رایگان",
|
freeConsultation: "دریافت مشاوره رایگان",
|
||||||
contactUs: "با ما تماس بگیرید",
|
contactUs: "با ما تماس بگیرید",
|
||||||
phoneNumber: "۰۸۶۹۱۰۰۹۰۰۱",
|
phoneNumber: "۰۸۶۹۱۰۰۹۰۰۱",
|
||||||
|
noProducts: "خدماتی برای نمایش وجود ندارد.",
|
||||||
|
inProgressOrders: "سفارش های در حال انجام",
|
||||||
},
|
},
|
||||||
rial: "ریال",
|
rial: "ریال",
|
||||||
uploadBox: {
|
uploadBox: {
|
||||||
|
|||||||
+7
-71
@@ -1,84 +1,20 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import Stats from './components/Stats'
|
import HomeSidebar from './components/HomeSidebar'
|
||||||
import Services from './components/Services'
|
|
||||||
import Orders from './components/Orders'
|
import Orders from './components/Orders'
|
||||||
import NewOrderImage from '@/assets/images/new_order.png'
|
import Services from './components/Services'
|
||||||
import { t } from '@/locale'
|
import Stats from './components/Stats'
|
||||||
import Button from '@/components/Button'
|
|
||||||
import { AddSquare, MessageQuestion } from 'iconsax-react'
|
|
||||||
import { COLORS } from '@/constants/colors'
|
|
||||||
import SupportImage from '@/assets/images/support1.png'
|
|
||||||
import { Paths } from '@/config/Paths'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
const Home: FC = () => {
|
const Home: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-6'>
|
<div className='flex flex-col gap-4 md:gap-6 xl:flex-row'>
|
||||||
<div className='flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<Stats />
|
<Stats />
|
||||||
<Services />
|
<Services />
|
||||||
<Orders />
|
<Orders />
|
||||||
</div>
|
</div>
|
||||||
<div className='min-w-[267px] mt-5'>
|
<HomeSidebar />
|
||||||
<div className='bg-white rounded-3xl p-6'>
|
|
||||||
<img src={NewOrderImage} className='w-[98px] mx-auto' />
|
|
||||||
<h4 className='mt-4 text-center font-medium'>
|
|
||||||
{t('home.submitNewOrder')}
|
|
||||||
</h4>
|
|
||||||
<Link to={Paths.newRequest}>
|
|
||||||
<Button
|
|
||||||
className='mt-2.5'>
|
|
||||||
<div className='flex gap-1'>
|
|
||||||
<AddSquare size={20} color='#292D32' />
|
|
||||||
<div className=''>{t('home.submitNewOrderButton')}</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-7 bg-white rounded-3xl p-6 relative'>
|
|
||||||
<h4>
|
|
||||||
{t('home.specialSolution')}
|
|
||||||
</h4>
|
|
||||||
<p className='mt-2 text-[#7B7E8B] text-xs'>
|
|
||||||
{t('home.specialSolutionDescription')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='mt-2 flex flex-col gap-2'>
|
|
||||||
<div className='flex gap-2 items-center text-xs'>
|
|
||||||
<div className='size-1.5 rounded-full bg-primary'></div>
|
|
||||||
<div>{t('home.specialSolutionFeature1')}</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex gap-2 items-center text-xs'>
|
|
||||||
<div className='size-1.5 rounded-full bg-primary'></div>
|
|
||||||
<div>{t('home.specialSolutionFeature2')}</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex gap-2 items-center text-xs'>
|
|
||||||
<div className='size-1.5 rounded-full bg-primary'></div>
|
|
||||||
<div>{t('home.specialSolutionFeature3')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
label={t('home.freeConsultation')}
|
|
||||||
className='mt-12'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='mt-2.5 bg-primary/11 py-6 px-4 rounded-[13px]'>
|
|
||||||
<div className='flex justify-between items-center'>
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<MessageQuestion size={20} color={COLORS.primary} />
|
|
||||||
<div className='text-xs font-light'>{t('home.contactUs')}</div>
|
|
||||||
</div>
|
|
||||||
<div>{t('home.phoneNumber')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img src={SupportImage} className='w-[50px] absolute bottom-[159px] left-2' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { clx } from '@/helpers/utils'
|
||||||
|
import type { CategoryType } from '@/pages/request/type/Types'
|
||||||
|
import { getCategoriesAtLevel } from '@/pages/request/utils/categoryUtils'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
type CategoryTabsProps = {
|
||||||
|
categories: CategoryType[]
|
||||||
|
levels: number[]
|
||||||
|
selectedPath: string[]
|
||||||
|
onSelect: (level: number, categoryId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryTabs: FC<CategoryTabsProps> = ({
|
||||||
|
categories,
|
||||||
|
levels,
|
||||||
|
selectedPath,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-3'>
|
||||||
|
{levels.map((level) => {
|
||||||
|
const items = getCategoriesAtLevel(categories, level, selectedPath)
|
||||||
|
|
||||||
|
if (!items.length) return null
|
||||||
|
|
||||||
|
const activeId = selectedPath[level] ?? ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={level}
|
||||||
|
className='-mx-4 overflow-x-auto px-4 md:mx-0 md:px-0'
|
||||||
|
>
|
||||||
|
<div className='flex w-max gap-2 md:gap-3'>
|
||||||
|
{items.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category.id}
|
||||||
|
type='button'
|
||||||
|
onClick={() => onSelect(level, category.id)}
|
||||||
|
className={clx(
|
||||||
|
'shrink-0 rounded-full px-4 py-2 text-xs transition-colors md:text-sm',
|
||||||
|
activeId === category.id
|
||||||
|
? 'bg-primary text-[#292D32]'
|
||||||
|
: 'bg-[#F5F5F7] text-[#7B7E8B] hover:bg-[#EBEBEF]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{category.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryTabs
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { clx } from '@/helpers/utils'
|
||||||
|
import { type FC, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
type HomeSectionProps = {
|
||||||
|
title?: string
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
contentClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const HomeSection: FC<HomeSectionProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={clx(
|
||||||
|
'rounded-2xl bg-white p-4 md:rounded-3xl md:p-6',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title ? (
|
||||||
|
<h2 className='text-base font-light md:text-lg'>{title}</h2>
|
||||||
|
) : null}
|
||||||
|
<div className={clx(title && 'mt-4 md:mt-6', contentClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomeSection
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import NewOrderCard from './NewOrderCard'
|
||||||
|
import SpecialSolutionCard from './SpecialSolutionCard'
|
||||||
|
|
||||||
|
const HomeSidebar: FC = () => {
|
||||||
|
return (
|
||||||
|
<aside className='flex w-full shrink-0 flex-col gap-4 md:gap-6 xl:mt-5 xl:w-[267px]'>
|
||||||
|
<NewOrderCard />
|
||||||
|
<SpecialSolutionCard />
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomeSidebar
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import NewOrderImage from '@/assets/images/new_order.png'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
import { t } from '@/locale'
|
||||||
|
import { AddSquare } from 'iconsax-react'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import HomeSection from './HomeSection'
|
||||||
|
|
||||||
|
const NewOrderCard: FC = () => {
|
||||||
|
return (
|
||||||
|
<HomeSection contentClassName='flex flex-col items-center'>
|
||||||
|
<img
|
||||||
|
src={NewOrderImage}
|
||||||
|
alt=''
|
||||||
|
className='mx-auto w-20 md:w-[98px]'
|
||||||
|
/>
|
||||||
|
<h3 className='mt-3 text-center text-sm font-medium md:mt-4 md:text-base'>
|
||||||
|
{t('home.submitNewOrder')}
|
||||||
|
</h3>
|
||||||
|
<Link to={Paths.newRequest} className='mt-2.5 w-full'>
|
||||||
|
<Button>
|
||||||
|
<span className='flex items-center justify-center gap-1'>
|
||||||
|
<AddSquare size={20} color='#292D32' />
|
||||||
|
{t('home.submitNewOrderButton')}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</HomeSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewOrderCard
|
||||||
@@ -1,83 +1,30 @@
|
|||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import { type FC } from 'react'
|
import { t } from '@/locale'
|
||||||
import { type ColumnType } from '@/components/types/TableTypes'
|
|
||||||
import { type Order } from './types/OrderTypes'
|
|
||||||
import { useGetMyOrders } from '@/pages/order/hooks/useOrderData'
|
|
||||||
import { TabMyOrdersEnum } from '@/pages/order/enum/OrderEnum'
|
import { TabMyOrdersEnum } from '@/pages/order/enum/OrderEnum'
|
||||||
import { orderStatusLabels } from '@/pages/order/constants/statusLabels'
|
import { useGetMyOrders } from '@/pages/order/hooks/useOrderData'
|
||||||
import moment from 'moment-jalaali'
|
import { type FC } from 'react'
|
||||||
import {
|
import HomeSection from './HomeSection'
|
||||||
OrderNumberRenderer,
|
import { mapOrderToRow, orderColumns } from './orderColumns'
|
||||||
OrderTitleRenderer,
|
import { type Order } from './types/OrderTypes'
|
||||||
OrderDateRenderer,
|
|
||||||
OrderStatusRenderer,
|
|
||||||
OrderMessagesRenderer,
|
|
||||||
OrderViewRenderer
|
|
||||||
} from './OrderTableComponents'
|
|
||||||
import type { OrderType } from '@/pages/order/type/Types'
|
|
||||||
|
|
||||||
const mapOrderToRow = (order: OrderType): Order => ({
|
|
||||||
id: order.id,
|
|
||||||
number: String(order.orderNumber ?? order.id),
|
|
||||||
title: order.title,
|
|
||||||
creationDate: {
|
|
||||||
gregorian: moment(order.createdAt).format('YYYY-MM-DD'),
|
|
||||||
persian: moment(order.createdAt).format('jYYYY/jMM/jDD')
|
|
||||||
},
|
|
||||||
status: orderStatusLabels[order.status] ?? order.status,
|
|
||||||
unreadMessages: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const Orders: FC = () => {
|
const Orders: FC = () => {
|
||||||
const { data, isLoading } = useGetMyOrders(1, TabMyOrdersEnum.IN_PROGRESS)
|
const { data, isLoading } = useGetMyOrders(1, TabMyOrdersEnum.IN_PROGRESS)
|
||||||
const orders: Order[] = (data?.data ?? []).map(mapOrderToRow)
|
const orders: Order[] = (data?.data ?? []).map(mapOrderToRow)
|
||||||
|
|
||||||
const columns: ColumnType<Order>[] = [
|
|
||||||
{
|
|
||||||
title: 'شماره',
|
|
||||||
key: 'number',
|
|
||||||
render: (order) => <OrderNumberRenderer order={order} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'عنوان سفارش',
|
|
||||||
key: 'title',
|
|
||||||
render: (order) => <OrderTitleRenderer order={order} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'تاریخ ایجاد',
|
|
||||||
key: 'creationDate',
|
|
||||||
render: (order) => <OrderDateRenderer order={order} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'وضعیت سفارش',
|
|
||||||
key: 'status',
|
|
||||||
render: (order) => <OrderStatusRenderer order={order} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'پیام ها',
|
|
||||||
key: 'unreadMessages',
|
|
||||||
render: (order) => <OrderMessagesRenderer order={order} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
key: 'actions',
|
|
||||||
render: (order) => <OrderViewRenderer order={order} />,
|
|
||||||
width: '60px'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-6 bg-white p-6 rounded-3xl'>
|
<HomeSection
|
||||||
<h4 className='text-lg font-light'>
|
title={t('home.inProgressOrders')}
|
||||||
سفارش های در حال انجام
|
className='mt-4 md:mt-6'
|
||||||
</h4>
|
contentClassName='mt-0 -mx-2 md:mx-0'
|
||||||
|
>
|
||||||
<Table<Order>
|
<div className='mt-4 overflow-x-auto md:mt-6'>
|
||||||
columns={columns}
|
<Table<Order>
|
||||||
data={orders}
|
columns={orderColumns}
|
||||||
isLoading={isLoading}
|
data={orders}
|
||||||
/>
|
isLoading={isLoading}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</HomeSection>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { type FC } from 'react'
|
|
||||||
import { type ProductType } from '@/pages/request/type/Types'
|
|
||||||
import PresignedImage from '@/components/PresignedImage'
|
import PresignedImage from '@/components/PresignedImage'
|
||||||
|
import { type ProductType } from '@/pages/request/type/Types'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
const ServiceItem: FC<{ product: ProductType }> = ({ product }) => {
|
const ServiceItem: FC<{ product: ProductType }> = ({ product }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='flex w-[88px] shrink-0 snap-start flex-col items-center gap-2 md:w-auto'>
|
||||||
<div className='size-20 rounded-full bg-gray-200 border-[3px] border-primary overflow-hidden'>
|
<div className='size-16 overflow-hidden rounded-full border-[3px] border-primary bg-gray-200 md:size-20'>
|
||||||
{product.images?.[0] ? (
|
{product.images?.[0] ? (
|
||||||
<PresignedImage
|
<PresignedImage
|
||||||
src={product.images[0]}
|
src={product.images[0]}
|
||||||
alt={product.title}
|
alt={product.title}
|
||||||
className='w-full h-full object-cover'
|
className='h-full w-full object-cover'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<span className='line-clamp-2 w-full text-center text-xs font-light text-[#292D32]'>
|
||||||
|
{product.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,121 @@
|
|||||||
import { t } from '@/locale'
|
import { t } from '@/locale'
|
||||||
import { type FC } from 'react'
|
import {
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
useGetCategories,
|
||||||
|
useGetProducts,
|
||||||
|
} from '@/pages/request/hooks/useRequestData'
|
||||||
|
import type { CategoryType } from '@/pages/request/type/Types'
|
||||||
|
import {
|
||||||
|
buildPathToFirstLeaf,
|
||||||
|
extractList,
|
||||||
|
getLevelsToShow,
|
||||||
|
} from '@/pages/request/utils/categoryUtils'
|
||||||
|
import { type FC, useEffect, useMemo, useState } from 'react'
|
||||||
|
import CategoryTabs from './CategoryTabs'
|
||||||
|
import HomeSection from './HomeSection'
|
||||||
import ServiceItem from './ServiceItem'
|
import ServiceItem from './ServiceItem'
|
||||||
import { useGetProducts } from '@/pages/request/hooks/useRequestData'
|
|
||||||
|
|
||||||
|
const SKELETON_COUNT = 6
|
||||||
|
|
||||||
|
const CategoryTabsSkeleton: FC = () => (
|
||||||
|
<div className='flex gap-2 overflow-hidden'>
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className='h-9 w-24 shrink-0 animate-pulse rounded-full bg-gray-100'
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ServicesSkeleton: FC = () => (
|
||||||
|
<div className='flex gap-4 md:grid md:grid-cols-4 md:gap-6 lg:grid-cols-6 xl:grid-cols-8'>
|
||||||
|
{Array.from({ length: SKELETON_COUNT }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className='flex w-[88px] shrink-0 flex-col items-center gap-2 md:w-auto'
|
||||||
|
>
|
||||||
|
<div className='size-16 animate-pulse rounded-full bg-gray-200 md:size-20' />
|
||||||
|
<div className='h-8 w-full animate-pulse rounded bg-gray-100' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const Services: FC = () => {
|
const Services: FC = () => {
|
||||||
const { data } = useGetProducts();
|
const { data: categoriesData, isLoading: isCategoriesLoading } =
|
||||||
|
useGetCategories()
|
||||||
|
const categories = useMemo(
|
||||||
|
() => extractList<CategoryType>(categoriesData),
|
||||||
|
[categoriesData],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!categories.length || selectedPath.length) return
|
||||||
|
|
||||||
|
setSelectedPath(
|
||||||
|
buildPathToFirstLeaf(categories, categories[0].id),
|
||||||
|
)
|
||||||
|
}, [categories, selectedPath.length])
|
||||||
|
|
||||||
|
const levelsToShow = useMemo(
|
||||||
|
() => getLevelsToShow(categories, selectedPath),
|
||||||
|
[categories, selectedPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeCategoryId = selectedPath.at(-1) ?? ''
|
||||||
|
|
||||||
|
const { data: productsData, isLoading: isProductsLoading } = useGetProducts(
|
||||||
|
activeCategoryId || undefined,
|
||||||
|
{ enabled: !!activeCategoryId },
|
||||||
|
)
|
||||||
|
const products = productsData?.data ?? []
|
||||||
|
|
||||||
|
const handleCategorySelect = (level: number, categoryId: string) => {
|
||||||
|
setSelectedPath(
|
||||||
|
buildPathToFirstLeaf(
|
||||||
|
categories,
|
||||||
|
categoryId,
|
||||||
|
selectedPath.slice(0, level),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = isCategoriesLoading || isProductsLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-6 bg-white p-6 rounded-3xl'>
|
<HomeSection title={t('home.products')} className='mt-4 md:mt-6' contentClassName='mt-0'>
|
||||||
<h4 className='text-lg font-light'>
|
<div className='mt-4 space-y-4 md:mt-6 md:space-y-6'>
|
||||||
{t('home.services')}
|
{isCategoriesLoading ? (
|
||||||
</h4>
|
<CategoryTabsSkeleton />
|
||||||
|
) : categories.length > 0 ? (
|
||||||
|
<CategoryTabs
|
||||||
|
categories={categories}
|
||||||
|
levels={levelsToShow}
|
||||||
|
selectedPath={selectedPath}
|
||||||
|
onSelect={handleCategorySelect}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='-mx-4 overflow-x-auto px-4 pb-1 md:mx-0 md:overflow-visible md:px-0'>
|
||||||
<Swiper
|
{isLoading ? (
|
||||||
spaceBetween={16}
|
<ServicesSkeleton />
|
||||||
slidesPerView={'auto'}
|
) : products.length === 0 ? (
|
||||||
className='h-full'
|
<p className='text-sm font-light text-[#7B7E8B]'>
|
||||||
breakpoints={{
|
{t('home.noProducts')}
|
||||||
640: {
|
</p>
|
||||||
spaceBetween: 24,
|
) : (
|
||||||
},
|
<div className='flex w-max snap-x snap-mandatory gap-4 md:grid md:w-full md:grid-cols-4 md:gap-6 md:snap-none lg:grid-cols-6 xl:grid-cols-8'>
|
||||||
}}
|
{products.map((item) => (
|
||||||
>
|
<ServiceItem key={item.id} product={item} />
|
||||||
{
|
))}
|
||||||
data?.data.map((item) => (
|
</div>
|
||||||
<SwiperSlide className='w-auto!' key={item.id}>
|
)}
|
||||||
<ServiceItem product={item} />
|
</div>
|
||||||
</SwiperSlide>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
</div>
|
||||||
|
</HomeSection>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Services
|
export default Services
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import SupportImage from '@/assets/images/support1.png'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { COLORS } from '@/constants/colors'
|
||||||
|
import { t } from '@/locale'
|
||||||
|
import { MessageQuestion } from 'iconsax-react'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import HomeSection from './HomeSection'
|
||||||
|
|
||||||
|
const FEATURE_KEYS = [
|
||||||
|
'home.specialSolutionFeature1',
|
||||||
|
'home.specialSolutionFeature2',
|
||||||
|
'home.specialSolutionFeature3',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const SpecialSolutionCard: FC = () => {
|
||||||
|
return (
|
||||||
|
<HomeSection>
|
||||||
|
<h3 className='text-sm font-medium md:text-base'>
|
||||||
|
{t('home.specialSolution')}
|
||||||
|
</h3>
|
||||||
|
<p className='mt-2 text-xs text-[#7B7E8B]'>
|
||||||
|
{t('home.specialSolutionDescription')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className='mt-3 flex flex-col gap-2'>
|
||||||
|
{FEATURE_KEYS.map((key) => (
|
||||||
|
<li key={key} className='flex items-center gap-2 text-xs'>
|
||||||
|
<span className='size-1.5 shrink-0 rounded-full bg-primary' />
|
||||||
|
{t(key)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('home.freeConsultation')}
|
||||||
|
className='mt-8 md:mt-12'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-3 flex items-end gap-3 md:mt-4'>
|
||||||
|
<img
|
||||||
|
src={SupportImage}
|
||||||
|
alt=''
|
||||||
|
className='w-10 shrink-0 md:w-[50px]'
|
||||||
|
/>
|
||||||
|
<div className='flex-1 rounded-[13px] bg-primary/11 px-4 py-4 md:py-6'>
|
||||||
|
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<MessageQuestion size={20} color={COLORS.primary} />
|
||||||
|
<span className='text-xs font-light'>
|
||||||
|
{t('home.contactUs')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className='text-sm font-medium' dir='ltr'>
|
||||||
|
{t('home.phoneNumber')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HomeSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpecialSolutionCard
|
||||||
@@ -4,34 +4,35 @@ import { type FC, type ReactNode } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon: ReactNode,
|
icon: ReactNode
|
||||||
count: number | undefined,
|
count: number | undefined
|
||||||
description: string,
|
description: string
|
||||||
to: string,
|
to: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatCard: FC<Props> = (props) => {
|
const StatCard: FC<Props> = ({ icon, count, description, to }) => {
|
||||||
const { icon, count, description, to } = props
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
className='flex-1 bg-white rounded-3xl p-6 block transition-shadow hover:shadow-md'
|
className='block flex-1 rounded-2xl bg-white p-4 transition-shadow hover:shadow-md md:rounded-3xl md:p-6'
|
||||||
>
|
>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex items-center justify-between'>
|
||||||
{icon}
|
{icon}
|
||||||
|
|
||||||
<div className='size-8 bg-[#FFF1D7] rounded-full flex justify-center items-center'>
|
<div className='flex size-7 items-center justify-center rounded-full bg-[#FFF1D7] md:size-8'>
|
||||||
<ArrowLeft size={16} color={COLORS.primary} className='rotate-45' />
|
<ArrowLeft
|
||||||
|
size={16}
|
||||||
|
color={COLORS.primary}
|
||||||
|
className='rotate-45'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-2 text-2xl font-semibold'>
|
<div className='mt-2 text-xl font-semibold md:text-2xl'>
|
||||||
{count}
|
{count ?? 0}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-2 text-sm'>
|
<div className='mt-1 text-xs md:mt-2 md:text-sm'>{description}</div>
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const Stats: FC = () => {
|
|||||||
mobile={2}
|
mobile={2}
|
||||||
gapDesktop={24}
|
gapDesktop={24}
|
||||||
gapMobile={12}
|
gapMobile={12}
|
||||||
className='mt-5'
|
className='mt-4 md:mt-5'
|
||||||
>
|
>
|
||||||
<StatCard
|
<StatCard
|
||||||
count={data?.data.requestsCount}
|
count={data?.data.requestsCount}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { type ColumnType } from '@/components/types/TableTypes'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { orderStatusLabels } from '@/pages/order/constants/statusLabels'
|
||||||
|
import type { OrderType } from '@/pages/order/type/Types'
|
||||||
|
import {
|
||||||
|
OrderDateRenderer,
|
||||||
|
OrderMessagesRenderer,
|
||||||
|
OrderNumberRenderer,
|
||||||
|
OrderStatusRenderer,
|
||||||
|
OrderTitleRenderer,
|
||||||
|
OrderViewRenderer,
|
||||||
|
} from './OrderTableComponents'
|
||||||
|
import { type Order } from './types/OrderTypes'
|
||||||
|
|
||||||
|
export const mapOrderToRow = (order: OrderType): Order => ({
|
||||||
|
id: order.id,
|
||||||
|
number: String(order.orderNumber ?? order.id),
|
||||||
|
title: order.title,
|
||||||
|
creationDate: {
|
||||||
|
gregorian: moment(order.createdAt).format('YYYY-MM-DD'),
|
||||||
|
persian: moment(order.createdAt).format('jYYYY/jMM/jDD'),
|
||||||
|
},
|
||||||
|
status: orderStatusLabels[order.status] ?? order.status,
|
||||||
|
unreadMessages: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const orderColumns: ColumnType<Order>[] = [
|
||||||
|
{
|
||||||
|
title: 'شماره',
|
||||||
|
key: 'number',
|
||||||
|
render: (order) => <OrderNumberRenderer order={order} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'عنوان سفارش',
|
||||||
|
key: 'title',
|
||||||
|
render: (order) => <OrderTitleRenderer order={order} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تاریخ ایجاد',
|
||||||
|
key: 'creationDate',
|
||||||
|
render: (order) => <OrderDateRenderer order={order} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'وضعیت سفارش',
|
||||||
|
key: 'status',
|
||||||
|
render: (order) => <OrderStatusRenderer order={order} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'پیام ها',
|
||||||
|
key: 'unreadMessages',
|
||||||
|
render: (order) => <OrderMessagesRenderer order={order} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
key: 'actions',
|
||||||
|
render: (order) => <OrderViewRenderer order={order} />,
|
||||||
|
width: '60px',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -2,67 +2,18 @@ import { type ChangeEvent, type FC, type SelectHTMLAttributes, useEffect, useMem
|
|||||||
import { useGetCategories, useGetProducts } from '../hooks/useRequestData'
|
import { useGetCategories, useGetProducts } from '../hooks/useRequestData'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
import type { CategoryType, ProductType } from '../type/Types'
|
import type { CategoryType, ProductType } from '../type/Types'
|
||||||
|
import {
|
||||||
|
extractList,
|
||||||
|
findCategoryById,
|
||||||
|
findCategoryPath,
|
||||||
|
getCategoriesAtLevel,
|
||||||
|
} from '../utils/categoryUtils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error_text?: string,
|
error_text?: string,
|
||||||
onProductSelect?: (product: ProductType | undefined) => void,
|
onProductSelect?: (product: ProductType | undefined) => void,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const findCategoryById = (categories: CategoryType[], targetId: string): CategoryType | null => {
|
|
||||||
for (const category of categories) {
|
|
||||||
if (category.id === targetId) {
|
|
||||||
return category
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.children?.length) {
|
|
||||||
const found = findCategoryById(category.children, targetId)
|
|
||||||
if (found) return found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const findCategoryPath = (categories: CategoryType[], targetId: string): string[] | null => {
|
|
||||||
for (const category of categories) {
|
|
||||||
if (category.id === targetId) {
|
|
||||||
return [category.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.children?.length) {
|
|
||||||
const childPath = findCategoryPath(category.children, targetId)
|
|
||||||
if (childPath) {
|
|
||||||
return [category.id, ...childPath]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCategoriesAtLevel = (
|
|
||||||
categories: CategoryType[],
|
|
||||||
level: number,
|
|
||||||
selectedPath: string[],
|
|
||||||
): CategoryType[] => {
|
|
||||||
if (level === 0) return categories
|
|
||||||
|
|
||||||
const parentId = selectedPath[level - 1]
|
|
||||||
if (!parentId) return []
|
|
||||||
|
|
||||||
const parent = findCategoryById(categories, parentId)
|
|
||||||
return parent?.children ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractList = <T,>(payload: unknown): T[] => {
|
|
||||||
if (Array.isArray(payload)) return payload
|
|
||||||
if (payload && typeof payload === 'object' && 'data' in payload) {
|
|
||||||
const maybeData = (payload as { data?: unknown }).data
|
|
||||||
return Array.isArray(maybeData) ? (maybeData as T[]) : []
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductsSelect: FC<Props> = (props) => {
|
const ProductsSelect: FC<Props> = (props) => {
|
||||||
const { error_text, onProductSelect, value, onChange, ...rest } = props
|
const { error_text, onProductSelect, value, onChange, ...rest } = props
|
||||||
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { CategoryType } from '../type/Types'
|
||||||
|
|
||||||
|
export const extractList = <T,>(payload: unknown): T[] => {
|
||||||
|
if (Array.isArray(payload)) return payload
|
||||||
|
if (payload && typeof payload === 'object' && 'data' in payload) {
|
||||||
|
const maybeData = (payload as { data?: unknown }).data
|
||||||
|
return Array.isArray(maybeData) ? (maybeData as T[]) : []
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findCategoryById = (
|
||||||
|
categories: CategoryType[],
|
||||||
|
targetId: string,
|
||||||
|
): CategoryType | null => {
|
||||||
|
for (const category of categories) {
|
||||||
|
if (category.id === targetId) {
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.children?.length) {
|
||||||
|
const found = findCategoryById(category.children, targetId)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findCategoryPath = (
|
||||||
|
categories: CategoryType[],
|
||||||
|
targetId: string,
|
||||||
|
): string[] | null => {
|
||||||
|
for (const category of categories) {
|
||||||
|
if (category.id === targetId) {
|
||||||
|
return [category.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.children?.length) {
|
||||||
|
const childPath = findCategoryPath(category.children, targetId)
|
||||||
|
if (childPath) {
|
||||||
|
return [category.id, ...childPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCategoriesAtLevel = (
|
||||||
|
categories: CategoryType[],
|
||||||
|
level: number,
|
||||||
|
selectedPath: string[],
|
||||||
|
): CategoryType[] => {
|
||||||
|
if (level === 0) return categories
|
||||||
|
|
||||||
|
const parentId = selectedPath[level - 1]
|
||||||
|
if (!parentId) return []
|
||||||
|
|
||||||
|
const parent = findCategoryById(categories, parentId)
|
||||||
|
return parent?.children ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLevelsToShow = (
|
||||||
|
categories: CategoryType[],
|
||||||
|
selectedPath: string[],
|
||||||
|
): number[] => {
|
||||||
|
const levels = [0]
|
||||||
|
|
||||||
|
for (let level = 0; level < selectedPath.length; level++) {
|
||||||
|
const category = findCategoryById(categories, selectedPath[level])
|
||||||
|
if (category?.children?.length) {
|
||||||
|
levels.push(level + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildPathToFirstLeaf = (
|
||||||
|
categories: CategoryType[],
|
||||||
|
categoryId: string,
|
||||||
|
prefixPath: string[] = [],
|
||||||
|
): string[] => {
|
||||||
|
const path = [...prefixPath, categoryId]
|
||||||
|
let current = findCategoryById(categories, categoryId)
|
||||||
|
|
||||||
|
while (current?.children?.length) {
|
||||||
|
path.push(current.children[0].id)
|
||||||
|
current = current.children[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user