refactor(FE): Refactor table skeleton components for consistency

This commit is contained in:
rstubryan
2026-03-02 12:10:06 +07:00
parent d3501e5f3d
commit 9c4c750664
10 changed files with 318 additions and 101 deletions
+14 -17
View File
@@ -351,19 +351,19 @@ const ClosingsTable = () => {
<span className='loading loading-spinner loading-xl' /> <span className='loading loading-spinner loading-xl' />
</div> </div>
) : data.length === 0 ? ( ) : data.length === 0 ? (
<ClosingTableSkeleton <div className='mt-3'>
columns={closingsColumns} <ClosingTableSkeleton
icon={ columns={closingsColumns}
<Icon icon={
icon='heroicons:chart-bar' <Icon
className='text-white' icon='heroicons:document-text'
width={20} className='text-white'
height={20} width={20}
/> height={20}
} />
title='Data Closing Belum Tersedia' }
subtitle='Tidak ada data closing untuk saat ini.' />
/> </div>
) : ( ) : (
<Table<Closing> <Table<Closing>
data={isResponseSuccess(closings) ? closings?.data : []} data={isResponseSuccess(closings) ? closings?.data : []}
@@ -382,10 +382,7 @@ const ClosingsTable = () => {
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
className={{ className={{
containerClassName: cn('mt-3', { containerClassName: cn('mt-3 mb-0'),
'w-full mb-0':
isResponseSuccess(closings) && closings?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap', headerColumnClassName: 'text-nowrap',
}} }}
/> />
@@ -6,13 +6,13 @@ import { ColumnDef } from '@tanstack/react-table';
const ClosingTableSkeleton = ({ const ClosingTableSkeleton = ({
columns, columns,
icon, icon,
title, title = 'No Data Available',
subtitle, subtitle = 'There is no closing data displayed. Enter closing data to get started.',
}: { }: {
columns: ColumnDef<Closing>[]; columns: ColumnDef<Closing>[];
icon: React.ReactNode; icon: React.ReactNode;
title: string; title?: string;
subtitle: string; subtitle?: string;
}) => { }) => {
return ( return (
<div className='relative size-full'> <div className='relative size-full'>
+42 -24
View File
@@ -25,6 +25,7 @@ import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWith
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { ExpenseApi } from '@/services/api/expense'; import { ExpenseApi } from '@/services/api/expense';
@@ -692,30 +693,47 @@ const ExpensesTable = () => {
{/* Table Section */} {/* Table Section */}
<div className='flex flex-col mb-4'> <div className='flex flex-col mb-4'>
<Table<Expense> {isLoading ? (
data={isResponseSuccess(expenses) ? expenses?.data : []} <div className='w-full flex flex-row justify-center items-center p-4'>
columns={expensesColumns} <span className='loading loading-spinner loading-xl' />
pageSize={tableFilterState.pageSize} </div>
page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} ) : !isResponseSuccess(expenses) || expenses.data?.length === 0 ? (
totalItems={ <div className='p-3'>
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 <ExpenseTableSkeleton
} columns={expensesColumns}
onPageChange={setPage} icon={
onPageSizeChange={setPageSize} <Icon
isLoading={isLoading} icon='heroicons:document-text'
sorting={sorting} className='text-white'
setSorting={setSorting} width={20}
rowSelection={rowSelection} height={20}
setRowSelection={setRowSelection} />
enableRowSelection={tableEnableRowSelectionHandler} }
className={{ />
containerClassName: cn('p-3 mb-0', { </div>
'w-full': ) : (
isResponseSuccess(expenses) && expenses?.data?.length === 0, <Table<Expense>
}), data={isResponseSuccess(expenses) ? expenses?.data : []}
headerColumnClassName: 'text-nowrap', columns={expensesColumns}
}} pageSize={tableFilterState.pageSize}
/> page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0}
totalItems={
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler}
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div> </div>
</div> </div>
@@ -0,0 +1,37 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { Expense } from '@/types/api/expense';
import { ColumnDef } from '@tanstack/react-table';
const ExpenseTableSkeleton = ({
columns,
icon,
title = 'No Data Available',
subtitle = 'There is no expense data displayed. Enter expense data to get started.',
}: {
columns: ColumnDef<Expense>[];
icon: React.ReactNode;
title?: string;
subtitle?: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default ExpenseTableSkeleton;
+16 -4
View File
@@ -44,6 +44,7 @@ import {
FinanceTableFilterSchema, FinanceTableFilterSchema,
FinanceTableFilterValues, FinanceTableFilterValues,
} from '@/components/pages/finance/filter/FinanceFilter'; } from '@/components/pages/finance/filter/FinanceFilter';
import FinanceTableSkeleton from '@/components/pages/finance/skeleton/FinanceTableSkeleton';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
popoverPosition = 'bottom', popoverPosition = 'bottom',
@@ -714,6 +715,20 @@ const FinanceTable = () => {
<div className='w-full flex flex-row justify-center items-center p-4'> <div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' /> <span className='loading loading-spinner loading-xl' />
</div> </div>
) : !isResponseSuccess(finances) || finances.data?.length === 0 ? (
<div className='p-3'>
<FinanceTableSkeleton
columns={columns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : ( ) : (
<Table<Finance> <Table<Finance>
data={isResponseSuccess(finances) ? finances.data : []} data={isResponseSuccess(finances) ? finances.data : []}
@@ -727,10 +742,7 @@ const FinanceTable = () => {
onPageSizeChange={setPageSize} onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
className={{ className={{
containerClassName: cn('p-3 mb-0', { containerClassName: cn('p-3 mb-0'),
'w-full':
isResponseSuccess(finances) && finances?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap', headerColumnClassName: 'text-nowrap',
}} }}
/> />
@@ -0,0 +1,37 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { Finance } from '@/types/api/finance/finance';
import { ColumnDef } from '@tanstack/react-table';
const FinanceTableSkeleton = ({
columns,
icon,
title = 'No Data Available',
subtitle = 'There is no finance data displayed. Enter finance data to get started.',
}: {
columns: ColumnDef<Finance>[];
icon: React.ReactNode;
title?: string;
subtitle?: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default FinanceTableSkeleton;
@@ -31,6 +31,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
import StatusBadge from '@/components/helper/StatusBadge'; import StatusBadge from '@/components/helper/StatusBadge';
import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter'; import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import MarketingTableSkeleton from '@/components/pages/marketing/skeleton/MarketingTableSkeleton';
const RowsOptionsMenu = ({ const RowsOptionsMenu = ({
props, props,
@@ -616,28 +617,49 @@ const MarketingTable = () => {
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
<Table <div className='flex flex-col mb-4'>
rowSelection={rowSelection} {isLoadingMarketing ? (
setRowSelection={setRowSelection} <div className='w-full flex flex-row justify-center items-center p-4'>
onPageChange={setPage} <span className='loading loading-spinner loading-xl' />
onPageSizeChange={setPageSize} </div>
data={allData} ) : !isResponseSuccess(marketing) || marketing.data?.length === 0 ? (
columns={columns} <div className='p-3'>
pageSize={tableFilterState.pageSize} <MarketingTableSkeleton
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1} columns={columns}
totalItems={ icon={
isResponseSuccess(marketing) ? marketing?.meta?.total_results : 0 <Icon
} icon='heroicons:document-text'
isLoading={isLoadingMarketing} className='text-white'
className={{ width={20}
containerClassName: cn('p-3', { height={20}
'w-full mb-20': />
isResponseSuccess(marketing) && marketing?.data?.length === 0, }
}), />
bodyColumnClassName: </div>
'last:text-end last:w-17 first:text-start first:w-5', ) : (
}} <Table
/> rowSelection={rowSelection}
setRowSelection={setRowSelection}
onPageChange={setPage}
onPageSizeChange={setPageSize}
data={allData}
columns={columns}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
totalItems={
isResponseSuccess(marketing)
? marketing?.meta?.total_results
: 0
}
isLoading={isLoadingMarketing}
className={{
containerClassName: cn('p-3 mb-0'),
bodyColumnClassName:
'last:text-end last:w-17 first:text-start first:w-5',
}}
/>
)}
</div>
</div> </div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -0,0 +1,37 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { Marketing } from '@/types/api/marketing/marketing';
import { ColumnDef } from '@tanstack/react-table';
const MarketingTableSkeleton = ({
columns,
icon,
title = 'No Data Available',
subtitle = 'There is no marketing data displayed. Enter marketing data to get started.',
}: {
columns: ColumnDef<Marketing>[];
icon: React.ReactNode;
title?: string;
subtitle?: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default MarketingTableSkeleton;
+50 -30
View File
@@ -17,6 +17,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge'; import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
@@ -441,36 +442,55 @@ const PurchaseTable = () => {
{/* Table Section */} {/* Table Section */}
<div className='flex flex-col mb-4'> <div className='flex flex-col mb-4'>
<Table<Purchase> {isLoading ? (
data={ <div className='w-full flex flex-row justify-center items-center p-4'>
isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] <span className='loading loading-spinner loading-xl' />
} </div>
columns={purchaseColumns} ) : !isResponseSuccess(purchaseRequests) ||
pageSize={tableFilterState.pageSize} purchaseRequests.data?.length === 0 ? (
page={ <div className='p-3'>
isResponseSuccess(purchaseRequests) <PurchaseTableSkeleton
? purchaseRequests?.meta?.page columns={purchaseColumns}
: 0 icon={
} <Icon
totalItems={ icon='heroicons:document-text'
isResponseSuccess(purchaseRequests) className='text-white'
? purchaseRequests?.meta?.total_results width={20}
: 0 height={20}
} />
onPageChange={setPage} }
onPageSizeChange={setPageSize} />
isLoading={isLoading} </div>
sorting={sorting} ) : (
setSorting={setSorting} <Table<Purchase>
className={{ data={
containerClassName: cn('p-3', { isResponseSuccess(purchaseRequests)
'w-full mb-20': ? purchaseRequests?.data
isResponseSuccess(purchaseRequests) && : []
purchaseRequests?.data?.length === 0, }
}), columns={purchaseColumns}
headerColumnClassName: 'text-nowrap', pageSize={tableFilterState.pageSize}
}} page={
/> isResponseSuccess(purchaseRequests)
? purchaseRequests?.meta?.page
: 0
}
totalItems={
isResponseSuccess(purchaseRequests)
? purchaseRequests?.meta?.total_results
: 0
}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div> </div>
</div> </div>
@@ -0,0 +1,37 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { Purchase } from '@/types/api/purchase/purchase';
import { ColumnDef } from '@tanstack/react-table';
const PurchaseTableSkeleton = ({
columns,
icon,
title = 'No Data Available',
subtitle = 'There is no purchase data displayed. Enter purchase data to get started.',
}: {
columns: ColumnDef<Purchase>[];
icon: React.ReactNode;
title?: string;
subtitle?: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default PurchaseTableSkeleton;