skeleton check loading

This commit is contained in:
hamid zarghami
2025-02-26 11:56:40 +03:30
parent 01f233248b
commit e3466bc951
13 changed files with 191 additions and 54 deletions
+1
View File
@@ -3,6 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
import 'swiper/swiper-bundle.css'; import 'swiper/swiper-bundle.css';
import 'rc-rate/assets/index.css'; import 'rc-rate/assets/index.css';
import './assets/fonts/roboto/roboto.css' import './assets/fonts/roboto/roboto.css'
import 'react-loading-skeleton/dist/skeleton.css'
import i18next from 'i18next' import i18next from 'i18next'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
+30
View File
@@ -0,0 +1,30 @@
import { FC, Fragment } from 'react'
import Td from './Td'
import Skeleton from 'react-loading-skeleton'
type Props = {
tdCount: number,
trCount?: number,
}
const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
return (
<Fragment>
{
Array.from({ length: trCount }).map((_, rowIndex) => (
<tr className="tr" key={rowIndex}>
{
Array.from({ length: tdCount }).map((_, colIndex) => (
<Td text={''} key={colIndex}>
<Skeleton />
</Td>
))
}
</tr>
))
}
</Fragment>
)
}
export default DefaultTableSkeleton
+1
View File
@@ -9,6 +9,7 @@ import { ItemServiceType } from '../pages/service/types/ServiecTypes'
type Props = { type Props = {
item: ItemServiceType, item: ItemServiceType,
className?: string, className?: string,
isLinkPanel?: boolean
} }
const ServiceItem: FC<Props> = (props: Props) => { const ServiceItem: FC<Props> = (props: Props) => {
+26
View File
@@ -199,3 +199,29 @@ tbody tr {
.has-number { .has-number {
font-family: Roboto !important; font-family: Roboto !important;
} }
.skeleton-box {
display: inline-block;
height: 1em;
position: relative;
overflow: hidden;
background-color: #dddbdd;
}
.skeleton-box:after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
rgba(#fff, 0) 0,
rgba(#fff, 0.2) 20%,
rgba(#fff, 0.5) 60%,
rgba(#fff, 0)
);
animation: shimmer 5s infinite;
content: "";
}
+5 -2
View File
@@ -244,7 +244,9 @@
"accepted": "تایید شده", "accepted": "تایید شده",
"sure_confrim_pay": "آیا مطمئن هستید که مبلغ صورتحساب از کیف پول شما کسر شود؟", "sure_confrim_pay": "آیا مطمئن هستید که مبلغ صورتحساب از کیف پول شما کسر شود؟",
"thank_pay_1": "ضمن تشکر از خرید شما", "thank_pay_1": "ضمن تشکر از خرید شما",
"thank_pay_2": "مبلغ این صورت حساب پرداخت شده است." "thank_pay_2": "مبلغ این صورت حساب پرداخت شده است.",
"discount_code": "کد تخفیف",
"apply": "اعمال"
}, },
"confrim": { "confrim": {
"subject": "حذف", "subject": "حذف",
@@ -372,7 +374,8 @@
"username_not_available": "نام کاربری موجود نیست", "username_not_available": "نام کاربری موجود نیست",
"username_save_success": "نام کاربری با موفقیت ذخیره شد", "username_save_success": "نام کاربری با موفقیت ذخیره شد",
"image_uploaded_successfully": "تصویر با موفقیت آپلود شد", "image_uploaded_successfully": "تصویر با موفقیت آپلود شد",
"confrim_email": "تایید ایمیل" "confrim_email": "تایید ایمیل",
"confrim": "تایید صورتحساب "
}, },
"email": "ایمیل", "email": "ایمیل",
"save": "ذخیره", "save": "ذخیره",
+24 -3
View File
@@ -14,6 +14,7 @@ import { clx } from '../../helpers/utils'
import Button from '../../components/Button' import Button from '../../components/Button'
import ApproveInvoice from './components/ApproveInvoice' import ApproveInvoice from './components/ApproveInvoice'
import PayInvoice from './components/PayInvoice' import PayInvoice from './components/PayInvoice'
import Input from '../../components/Input'
const ReceiptsDetail: FC = () => { const ReceiptsDetail: FC = () => {
@@ -24,10 +25,30 @@ const ReceiptsDetail: FC = () => {
return ( return (
<div className='mt-4'> <div className='mt-4'>
<div className='flex gap-6 items-center'> <div className='flex xl:flex-row flex-col gap-4 items-center'>
<div>{t('receip.detail_account')}</div> <div className='flex gap-6 items-center'>
<div className='whitespace-nowrap'>{t('receip.detail_account')}</div>
<ApproveInvoice status={getInvoce.data?.data?.invoice?.status} id={id ? id : ''} refetch={getInvoce.refetch} /> <ApproveInvoice status={getInvoce.data?.data?.invoice?.status} id={id ? id : ''} refetch={getInvoce.refetch} />
</div>
{
getInvoce.data?.data?.invoice?.status === 'PENDING' || getInvoce.data?.data?.invoice?.status === 'APPROVED' ?
<div className='flex gap-2 items-center w-full'>
<div className='w-full xl:w-fit'>
<Input
className='bg-transparent border border-border xl:max-w-40'
placeholder={t('receip.discount_code')}
/>
</div>
<Button
label={t('receip.apply')}
className='bg-transparent text-black border border-black mt-1 px-5 w-fit'
/>
</div>
: null
}
</div> </div>
{ {
@@ -58,7 +58,7 @@ const ApproveInvoice: FC<Props> = ({ id, refetch, status }) => {
return ( return (
<Fragment> <Fragment>
<Button <Button
isLoading={approveInvoce.isPending || requestApprove.isPending} disabled={approveInvoce.isPending || requestApprove.isPending}
onClick={() => status === 'PENDING' ? handleShowModal() : null} onClick={() => status === 'PENDING' ? handleShowModal() : null}
className={clx( className={clx(
'px-9 w-fit bg-white bg-opacity-70 text-description', 'px-9 w-fit bg-white bg-opacity-70 text-description',
@@ -70,7 +70,7 @@ const ApproveInvoice: FC<Props> = ({ id, refetch, status }) => {
size={20} size={20}
color={status === 'PENDING' ? '#888888' : '#00BA4B'} color={status === 'PENDING' ? '#888888' : '#00BA4B'}
/> />
<div> <div className='whitespace-nowrap'>
{ {
status === 'APPROVED' ? status === 'APPROVED' ?
t('receip.accepted') t('receip.accepted')
@@ -85,7 +85,7 @@ const ApproveInvoice: FC<Props> = ({ id, refetch, status }) => {
open={showModal} open={showModal}
close={() => setShowModal(false)} close={() => setShowModal(false)}
isHeader isHeader
title_header={t('profile.confrim_email')} title_header={t('profile.confrim')}
> >
<div className='mt-7'> <div className='mt-7'>
<div className='text-xs text-center'> <div className='text-xs text-center'>
@@ -121,6 +121,7 @@ const ApproveInvoice: FC<Props> = ({ id, refetch, status }) => {
className='w-[150px] text-xs' className='w-[150px] text-xs'
onClick={handleConfrim} onClick={handleConfrim}
disabled={code.length !== 5} disabled={code.length !== 5}
isLoading={approveInvoce.isPending}
> >
<div className='flex gap-2 items-center'> <div className='flex gap-2 items-center'>
<TickCircle <TickCircle
@@ -1,6 +1,7 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/ReceiptService"; import * as api from "../service/ReceiptService";
import { import {
ApplyDiscountType,
ApproveInvoiceType, ApproveInvoiceType,
RequestApproveInvoiceType, RequestApproveInvoiceType,
} from "../types/ReceiptTypes"; } from "../types/ReceiptTypes";
@@ -39,3 +40,9 @@ export const useRequestApprove = () => {
api.requestApprove(variables), api.requestApprove(variables),
}); });
}; };
export const useApplyDiscount = () => {
return useMutation({
mutationFn: (variables: ApplyDiscountType) => api.applyDiscount(variables),
});
};
@@ -1,5 +1,6 @@
import axios from "../../../config/axios"; import axios from "../../../config/axios";
import { import {
ApplyDiscountType,
ApproveInvoiceType, ApproveInvoiceType,
RequestApproveInvoiceType, RequestApproveInvoiceType,
} from "../types/ReceiptTypes"; } from "../types/ReceiptTypes";
@@ -31,3 +32,11 @@ export const payInvoice = async (id: string) => {
const { data } = await axios.post(`/invoices/${id}/pay`); const { data } = await axios.post(`/invoices/${id}/pay`);
return data; return data;
}; };
export const applyDiscount = async (params: ApplyDiscountType) => {
const { data } = await axios.post(
`/invoices/${params.id}/apply-discount`,
params
);
return data;
};
+5
View File
@@ -40,3 +40,8 @@ export type ApproveInvoiceType = {
code: string; code: string;
id: string; id: string;
}; };
export type ApplyDiscountType = {
id: string;
discountCode: string;
};
+52 -45
View File
@@ -4,11 +4,12 @@ import { useTranslation } from 'react-i18next'
import Td from '../../components/Td' import Td from '../../components/Td'
import Detail from './components/Detail' import Detail from './components/Detail'
import { useGetTransactions } from './hooks/useTransactionData' import { useGetTransactions } from './hooks/useTransactionData'
import PageLoading from '../../components/PageLoading'
import { TransactionItemType } from './types/TransactionTypes' import { TransactionItemType } from './types/TransactionTypes'
import moment from 'moment-jalaali' import moment from 'moment-jalaali'
import { NumberFormat } from '../../config/func' import { NumberFormat } from '../../config/func'
import Pagination from '../../components/Pagination' import Pagination from '../../components/Pagination'
import BoxSekeleton from './skeleton/BoxSekeleton'
import DefaultTableSkeleton from '../../components/DefaultTableSkeleton'
const TransactionList: FC = () => { const TransactionList: FC = () => {
@@ -22,12 +23,14 @@ const TransactionList: FC = () => {
{t('transaction.transaction')} {t('transaction.transaction')}
</div> </div>
{
getTransactions.isPending ? <Fragment>
<PageLoading /> {
: getTransactions.isPending ?
<Fragment> <BoxSekeleton />
:
<div className='mt-6 flex xl:gap-6 gap-1 justify-center'> <div className='mt-6 flex xl:gap-6 gap-1 justify-center'>
<div className='bg-white flex-1 rounded-2xl xl:p-6 py-2 px-4 xl:max-w-[253px]'> <div className='bg-white flex-1 rounded-2xl xl:p-6 py-2 px-4 xl:max-w-[253px]'>
<div className='flex xl:flex-row flex-col gap-2 items-center'> <div className='flex xl:flex-row flex-col gap-2 items-center'>
<div className='size-8 bg-[#EEF0F7] rounded-full flex justify-center items-center'> <div className='size-8 bg-[#EEF0F7] rounded-full flex justify-center items-center'>
@@ -86,48 +89,52 @@ const TransactionList: FC = () => {
</div> </div>
</div> </div>
</div> </div>
}
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'> <div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<table className='w-full text-sm '> <table className='w-full text-sm '>
<thead className='thead'> <thead className='thead'>
<tr> <tr>
<Td text={t('ticket.number')} /> <Td text={t('ticket.number')} />
<Td text={t('transaction.description')} /> <Td text={t('transaction.description')} />
<Td text={t('transaction.amount')} /> <Td text={t('transaction.amount')} />
<Td text={t('ticket.date')} /> <Td text={t('ticket.date')} />
<Td text={t('ticket.detail')} /> <Td text={t('ticket.detail')} />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
getTransactions.data?.data?.transactions?.map((item: TransactionItemType) => { getTransactions.isPending &&
return ( <DefaultTableSkeleton tdCount={5} />
<tr key={item.id}> }
<Td text={item.numericId + ''} /> {
<Td text={item.description} /> getTransactions.data?.data?.transactions?.map((item: TransactionItemType) => {
<Td text={NumberFormat(+item.amount)} /> return (
<Td text={moment(item.createdAt).format('jYYYY-jMM-jDD')} /> <tr className='tr' key={item.id}>
<Td text={''}> <Td text={item.numericId + ''} />
<Detail /> <Td text={item.description} />
</Td> <Td text={NumberFormat(+item.amount)} />
</tr> <Td text={moment(item.createdAt).format('jYYYY-jMM-jDD')} />
) <Td text={''}>
}) <Detail />
} </Td>
</tbody> </tr>
</table> )
</div> })
}
</tbody>
</table>
</div>
<div className='flex justify-end'> <div className='flex justify-end'>
<Pagination <Pagination
currentPage={page} currentPage={page}
totalPages={getTransactions.data?.data?.pager?.totalPages} totalPages={getTransactions.data?.data?.pager?.totalPages}
onPageChange={setPage} onPageChange={setPage}
/> />
</div> </div>
</Fragment> </Fragment>
}
</div > </div >
) )
} }
@@ -0,0 +1,26 @@
import { FC } from 'react'
import Skeleton from 'react-loading-skeleton'
const BoxSekeleton: FC = () => {
return (
<div className='mt-6 flex xl:gap-6 gap-1 justify-center'>
<Skeleton
containerClassName='flex-1 xl:p-6 py-2 px-4 xl:max-w-[253px]'
className='h-[111px] !rounded-2xl'
baseColor='#D9D9D9'
/>
<Skeleton
containerClassName='flex-1 xl:p-6 py-2 px-4 xl:max-w-[253px]'
className='h-[111px] !rounded-2xl'
baseColor='#D9D9D9'
/>
<Skeleton
containerClassName='flex-1 xl:p-6 py-2 px-4 xl:max-w-[253px]'
className='h-[111px] !rounded-2xl'
baseColor='#D9D9D9'
/>
</div>
)
}
export default BoxSekeleton
+1 -1
View File
@@ -36,7 +36,7 @@ const MainRouter: FC = () => {
<Header /> <Header />
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'> <div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
<div className={`overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}> <div className={`overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
<div className='pb-16'> <div className='pb-20'>
<Routes> <Routes>
<Route path={Pages.dashboard} element={<Home />} /> <Route path={Pages.dashboard} element={<Home />} />
<Route path={Pages.services.mine} element={<MyServices />} /> <Route path={Pages.services.mine} element={<MyServices />} />