feat: enhance CreditTransactions component with loading skeleton and refactor state management using useQueryStates

This commit is contained in:
javayhu 2025-08-28 01:08:48 +08:00
parent 797ee9b7e5
commit 6c584c75e2
5 changed files with 100 additions and 29 deletions

View File

@ -44,17 +44,30 @@ export const getCreditTransactionsAction = userActionClient
const { pageIndex, pageSize, search, sorting } = parsedInput; const { pageIndex, pageSize, search, sorting } = parsedInput;
const currentUser = (ctx as { user: User }).user; const currentUser = (ctx as { user: User }).user;
// search by type, amount, paymentId, description, and restrict to current user // Search logic: text fields use ilike, and if search is a number, also search amount fields
const searchConditions = [];
if (search) {
// Always search text fields
searchConditions.push(
ilike(creditTransaction.type, `%${search}%`),
ilike(creditTransaction.paymentId, `%${search}%`),
ilike(creditTransaction.description, `%${search}%`)
);
// If search is a valid number, also search numeric fields
const numericSearch = Number.parseInt(search, 10);
if (!Number.isNaN(numericSearch)) {
searchConditions.push(
eq(creditTransaction.amount, numericSearch),
eq(creditTransaction.remainingAmount, numericSearch)
);
}
}
const where = search const where = search
? and( ? and(
eq(creditTransaction.userId, currentUser.id), eq(creditTransaction.userId, currentUser.id),
or( or(...searchConditions)
ilike(creditTransaction.type, `%${search}%`),
ilike(creditTransaction.amount, `%${search}%`),
ilike(creditTransaction.remainingAmount, `%${search}%`),
ilike(creditTransaction.paymentId, `%${search}%`),
ilike(creditTransaction.description, `%${search}%`)
)
) )
: eq(creditTransaction.userId, currentUser.id); : eq(creditTransaction.userId, currentUser.id);

View File

@ -515,7 +515,7 @@ export function UsersTable({
<SelectValue placeholder={pageSize} /> <SelectValue placeholder={pageSize} />
</SelectTrigger> </SelectTrigger>
<SelectContent side="top"> <SelectContent side="top">
{[5, 10, 20, 30, 40, 50].map((pageSize) => ( {[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}> <SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize} {pageSize}
</SelectItem> </SelectItem>

View File

@ -76,6 +76,7 @@ import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Badge } from '../../ui/badge'; import { Badge } from '../../ui/badge';
import { Label } from '../../ui/label'; import { Label } from '../../ui/label';
import { Skeleton } from '../../ui/skeleton';
// Define the credit transaction interface // Define the credit transaction interface
export interface CreditTransaction { export interface CreditTransaction {
@ -152,12 +153,27 @@ function DataTableColumnHeader<TData, TValue>({
); );
} }
function TableRowSkeleton({ columns }: { columns: number }) {
return (
<TableRow>
{Array.from({ length: columns }).map((_, index) => (
<TableCell key={index} className="py-4">
<div className="flex items-center gap-2 pl-3">
<Skeleton className="h-6 w-full max-w-32" />
</div>
</TableCell>
))}
</TableRow>
);
}
interface CreditTransactionsTableProps { interface CreditTransactionsTableProps {
data: CreditTransaction[]; data: CreditTransaction[];
total: number; total: number;
pageIndex: number; pageIndex: number;
pageSize: number; pageSize: number;
search: string; search: string;
sorting?: SortingState;
loading?: boolean; loading?: boolean;
onSearch: (search: string) => void; onSearch: (search: string) => void;
onPageChange: (page: number) => void; onPageChange: (page: number) => void;
@ -171,6 +187,7 @@ export function CreditTransactionsTable({
pageIndex, pageIndex,
pageSize, pageSize,
search, search,
sorting = [{ id: 'createdAt', desc: true }],
loading, loading,
onSearch, onSearch,
onPageChange, onPageChange,
@ -179,9 +196,6 @@ export function CreditTransactionsTable({
}: CreditTransactionsTableProps) { }: CreditTransactionsTableProps) {
const t = useTranslations('Dashboard.settings.credits.transactions'); const t = useTranslations('Dashboard.settings.credits.transactions');
const tTable = useTranslations('Common.table'); const tTable = useTranslations('Common.table');
const [sorting, setSorting] = useState<SortingState>([
{ id: 'createdAt', desc: true },
]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
@ -449,7 +463,6 @@ export function CreditTransactionsTable({
}, },
onSortingChange: (updater) => { onSortingChange: (updater) => {
const next = typeof updater === 'function' ? updater(sorting) : updater; const next = typeof updater === 'function' ? updater(sorting) : updater;
setSorting(next);
onSortingChange?.(next); onSortingChange?.(next);
}, },
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
@ -538,7 +551,12 @@ export function CreditTransactionsTable({
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows?.length ? ( {loading ? (
// Show skeleton rows while loading
Array.from({ length: pageSize }).map((_, index) => (
<TableRowSkeleton key={index} columns={columns.length} />
))
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
@ -560,7 +578,7 @@ export function CreditTransactionsTable({
colSpan={columns.length} colSpan={columns.length}
className="h-24 text-center" className="h-24 text-center"
> >
{loading ? tTable('loading') : tTable('noResults')} {tTable('noResults')}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}

View File

@ -4,22 +4,36 @@ import { CreditTransactionsTable } from '@/components/settings/credits/credit-tr
import { useCreditTransactions } from '@/hooks/use-credits'; import { useCreditTransactions } from '@/hooks/use-credits';
import type { SortingState } from '@tanstack/react-table'; import type { SortingState } from '@tanstack/react-table';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useState } from 'react'; import {
parseAsIndex,
parseAsInteger,
parseAsString,
useQueryStates,
} from 'nuqs';
import { useMemo } from 'react';
/** /**
* Credit transactions component * Credit transactions component
*/ */
export function CreditTransactions() { export function CreditTransactions() {
const t = useTranslations('Dashboard.settings.credits.transactions'); const t = useTranslations('Dashboard.settings.credits.transactions');
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10); const [{ page, pageSize, search, sortId, sortDesc }, setQueryStates] =
const [search, setSearch] = useState(''); useQueryStates({
const [sorting, setSorting] = useState<SortingState>([ page: parseAsIndex.withDefault(0), // 0-based internally, 1-based in URL
{ id: 'createdAt', desc: true }, pageSize: parseAsInteger.withDefault(10),
]); search: parseAsString.withDefault(''),
sortId: parseAsString.withDefault('createdAt'),
sortDesc: parseAsInteger.withDefault(1),
});
const sorting: SortingState = useMemo(
() => [{ id: sortId, desc: Boolean(sortDesc) }],
[sortId, sortDesc]
);
const { data, isLoading } = useCreditTransactions( const { data, isLoading } = useCreditTransactions(
pageIndex, page,
pageSize, pageSize,
search, search,
sorting sorting
@ -29,14 +43,24 @@ export function CreditTransactions() {
<CreditTransactionsTable <CreditTransactionsTable
data={data?.items || []} data={data?.items || []}
total={data?.total || 0} total={data?.total || 0}
pageIndex={pageIndex} pageIndex={page}
pageSize={pageSize} pageSize={pageSize}
search={search} search={search}
sorting={sorting}
loading={isLoading} loading={isLoading}
onSearch={setSearch} onSearch={(newSearch) => setQueryStates({ search: newSearch, page: 0 })}
onPageChange={setPageIndex} onPageChange={(newPageIndex) => setQueryStates({ page: newPageIndex })}
onPageSizeChange={setPageSize} onPageSizeChange={(newPageSize) =>
onSortingChange={setSorting} setQueryStates({ pageSize: newPageSize, page: 0 })
}
onSortingChange={(newSorting) => {
if (newSorting.length > 0) {
setQueryStates({
sortId: newSorting[0].id,
sortDesc: newSorting[0].desc ? 1 : 0,
});
}
}}
/> />
); );
} }

View File

@ -5,6 +5,7 @@ import { CreditTransactions } from '@/components/settings/credits/credit-transac
import CreditsBalanceCard from '@/components/settings/credits/credits-balance-card'; import CreditsBalanceCard from '@/components/settings/credits/credits-balance-card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { parseAsStringLiteral, useQueryState } from 'nuqs';
/** /**
* Credits page client, show credit balance and transactions * Credits page client, show credit balance and transactions
@ -12,9 +13,24 @@ import { useTranslations } from 'next-intl';
export default function CreditsPageClient() { export default function CreditsPageClient() {
const t = useTranslations('Dashboard.settings.credits'); const t = useTranslations('Dashboard.settings.credits');
const [activeTab, setActiveTab] = useQueryState(
'tab',
parseAsStringLiteral(['balance', 'transactions']).withDefault('balance')
);
const handleTabChange = (value: string) => {
if (value === 'balance' || value === 'transactions') {
setActiveTab(value);
}
};
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<Tabs defaultValue="balance" className="w-full"> <Tabs
value={activeTab}
onValueChange={handleTabChange}
className="w-full"
>
<TabsList className=""> <TabsList className="">
<TabsTrigger value="balance">{t('tabs.balance')}</TabsTrigger> <TabsTrigger value="balance">{t('tabs.balance')}</TabsTrigger>
<TabsTrigger value="transactions"> <TabsTrigger value="transactions">