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
@@ -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 ? (
<div className='mt-3'>
<ClosingTableSkeleton <ClosingTableSkeleton
columns={closingsColumns} columns={closingsColumns}
icon={ icon={
<Icon <Icon
icon='heroicons:chart-bar' icon='heroicons:document-text'
className='text-white' className='text-white'
width={20} width={20}
height={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'>
+22 -4
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,6 +693,25 @@ const ExpensesTable = () => {
{/* Table Section */} {/* Table Section */}
<div className='flex flex-col mb-4'> <div className='flex flex-col mb-4'>
{isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : !isResponseSuccess(expenses) || expenses.data?.length === 0 ? (
<div className='p-3'>
<ExpenseTableSkeleton
columns={expensesColumns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : (
<Table<Expense> <Table<Expense>
data={isResponseSuccess(expenses) ? expenses?.data : []} data={isResponseSuccess(expenses) ? expenses?.data : []}
columns={expensesColumns} columns={expensesColumns}
@@ -709,13 +729,11 @@ const ExpensesTable = () => {
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler} enableRowSelection={tableEnableRowSelectionHandler}
className={{ className={{
containerClassName: cn('p-3 mb-0', { containerClassName: cn('p-3 mb-0'),
'w-full':
isResponseSuccess(expenses) && expenses?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap', 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,6 +617,26 @@ const MarketingTable = () => {
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
<div className='flex flex-col mb-4'>
{isLoadingMarketing ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : !isResponseSuccess(marketing) || marketing.data?.length === 0 ? (
<div className='p-3'>
<MarketingTableSkeleton
columns={columns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : (
<Table <Table
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
@@ -626,18 +647,19 @@ const MarketingTable = () => {
pageSize={tableFilterState.pageSize} pageSize={tableFilterState.pageSize}
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1} page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
totalItems={ totalItems={
isResponseSuccess(marketing) ? marketing?.meta?.total_results : 0 isResponseSuccess(marketing)
? marketing?.meta?.total_results
: 0
} }
isLoading={isLoadingMarketing} isLoading={isLoadingMarketing}
className={{ className={{
containerClassName: cn('p-3', { containerClassName: cn('p-3 mb-0'),
'w-full mb-20':
isResponseSuccess(marketing) && marketing?.data?.length === 0,
}),
bodyColumnClassName: bodyColumnClassName:
'last:text-end last:w-17 first:text-start first:w-5', '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;
@@ -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,9 +442,31 @@ const PurchaseTable = () => {
{/* Table Section */} {/* Table Section */}
<div className='flex flex-col mb-4'> <div className='flex flex-col mb-4'>
{isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : !isResponseSuccess(purchaseRequests) ||
purchaseRequests.data?.length === 0 ? (
<div className='p-3'>
<PurchaseTableSkeleton
columns={purchaseColumns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : (
<Table<Purchase> <Table<Purchase>
data={ data={
isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] isResponseSuccess(purchaseRequests)
? purchaseRequests?.data
: []
} }
columns={purchaseColumns} columns={purchaseColumns}
pageSize={tableFilterState.pageSize} pageSize={tableFilterState.pageSize}
@@ -463,14 +486,11 @@ const PurchaseTable = () => {
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn('p-3', { containerClassName: cn('p-3 mb-0'),
'w-full mb-20':
isResponseSuccess(purchaseRequests) &&
purchaseRequests?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap', 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;