paginagtion desingn finished
This commit is contained in:
parent
98ab87fcbd
commit
569fdc5125
@ -1,5 +1,5 @@
|
|||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||||
import { Button } from '../buttons/Button';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export interface PaginationProps {
|
export interface PaginationProps {
|
||||||
/** 1-based current page. */
|
/** 1-based current page. */
|
||||||
@ -8,38 +8,114 @@ export interface PaginationProps {
|
|||||||
/** Total rows across all pages. */
|
/** Total rows across all pages. */
|
||||||
total: number;
|
total: number;
|
||||||
onPage: (page: number) => void;
|
onPage: (page: number) => void;
|
||||||
|
onPageSizeChange?: (size: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pager: "Showing a–b of N" + Prev/Next. Renders nothing when the set fits on
|
/** Pager: "Showing a–b of N" + Prev/Next. Renders nothing when the set fits on
|
||||||
* one page. Shared by the record-view tables. */
|
* one page. Shared by the record-view tables. */
|
||||||
export function Pagination({ page, pageSize, total, onPage }: PaginationProps) {
|
export function Pagination({ page, pageSize, total, onPage, onPageSizeChange }: PaginationProps) {
|
||||||
if (total <= 0) return null;
|
if (total <= 0) return null;
|
||||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||||
const safePage = Math.min(Math.max(page, 1), totalPages);
|
const safePage = Math.min(Math.max(page, 1), totalPages);
|
||||||
const start = (safePage - 1) * pageSize;
|
const start = (safePage - 1) * pageSize;
|
||||||
|
|
||||||
|
const [jumpPage, setJumpPage] = useState(safePage.toString());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setJumpPage(safePage.toString());
|
||||||
|
}, [safePage]);
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
onPage(newPage);
|
onPage(newPage);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-3 border-t border-border-subtle px-[18px] pt-4 pb-24">
|
<div className="flex flex-col items-center justify-center gap-4 pt-5 pb-24 border-t border-gray-100 mt-2">
|
||||||
<span className="text-xs text-faint">
|
<div className="flex items-center bg-white shadow-sm border border-gray-200 rounded-full p-1.5 gap-1 overflow-x-auto max-w-full no-scrollbar">
|
||||||
Showing {start + 1}–{Math.min(start + pageSize, total)} of {total}
|
<button
|
||||||
|
disabled={safePage <= 1}
|
||||||
|
onClick={() => handlePageChange(1)}
|
||||||
|
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-full transition-all ${
|
||||||
|
safePage <= 1 ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:bg-gray-100 active:scale-95'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronsLeft size={20} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={safePage <= 1}
|
||||||
|
onClick={() => handlePageChange(safePage - 1)}
|
||||||
|
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-full transition-all ${
|
||||||
|
safePage <= 1 ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:bg-gray-100 active:scale-95'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="px-3 text-sm font-bold text-gray-700 bg-gray-50 h-10 flex flex-shrink-0 items-center justify-center rounded-full border border-gray-100 tracking-wide whitespace-nowrap">
|
||||||
|
<span className="text-gray-400 mr-2 font-medium">PAGE</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={totalPages}
|
||||||
|
value={jumpPage}
|
||||||
|
onChange={(e) => setJumpPage(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
let p = parseInt(jumpPage, 10);
|
||||||
|
if (isNaN(p)) p = safePage;
|
||||||
|
p = Math.max(1, Math.min(p, totalPages));
|
||||||
|
setJumpPage(p.toString());
|
||||||
|
if (p !== safePage) handlePageChange(p);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-12 h-7 bg-white border border-gray-200 rounded text-center focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 font-bold text-gray-900 p-0 m-0 no-spinners"
|
||||||
|
/>
|
||||||
|
<span className="text-gray-400 mx-2 font-medium">/</span> {totalPages}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={safePage >= totalPages}
|
||||||
|
onClick={() => handlePageChange(safePage + 1)}
|
||||||
|
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-full transition-all ${
|
||||||
|
safePage >= totalPages ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:bg-gray-100 active:scale-95'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronRight size={20} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={safePage >= totalPages}
|
||||||
|
onClick={() => handlePageChange(totalPages)}
|
||||||
|
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-full transition-all ${
|
||||||
|
safePage >= totalPages ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:bg-gray-100 active:scale-95'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronsRight size={20} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 px-4 w-full">
|
||||||
|
<span className="text-[11px] uppercase tracking-wider font-bold text-gray-400 text-center">
|
||||||
|
Showing {start + 1} to {Math.min(start + pageSize, total)} of {total.toLocaleString()} Records
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="secondary" size="sm" disabled={safePage <= 1} onClick={() => handlePageChange(safePage - 1)}>
|
{onPageSizeChange && (
|
||||||
<ChevronLeft size={15} />
|
<select
|
||||||
Prev
|
value={pageSize}
|
||||||
</Button>
|
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||||
<span className="px-1 text-xs font-medium text-muted nums">
|
className="text-[11px] font-bold text-gray-600 bg-gray-50 border border-gray-200 rounded-md px-2 py-1.5 outline-none focus:ring-1 focus:ring-blue-500 uppercase tracking-wider cursor-pointer"
|
||||||
{safePage} / {totalPages}
|
>
|
||||||
</span>
|
<option value={10}>10 / PAGE</option>
|
||||||
<Button variant="secondary" size="sm" disabled={safePage >= totalPages} onClick={() => handlePageChange(safePage + 1)}>
|
<option value={20}>20 / PAGE</option>
|
||||||
Next
|
<option value={50}>50 / PAGE</option>
|
||||||
<ChevronRight size={15} />
|
<option value={100}>100 / PAGE</option>
|
||||||
</Button>
|
</select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -69,6 +69,7 @@ export function RecordView({
|
|||||||
preset_alias,
|
preset_alias,
|
||||||
}: RecordViewProps) {
|
}: RecordViewProps) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const [activePageSize, setActivePageSize] = useState(pageSize);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [debounced, setDebounced] = useState('');
|
const [debounced, setDebounced] = useState('');
|
||||||
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
|
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
|
||||||
@ -107,7 +108,7 @@ export function RecordView({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const r = await client.recordView(rvUid, { preset_alias, page, limit: pageSize, search: debounced, filters: filtersParam, sortBy, sortDir });
|
const r = await client.recordView(rvUid, { preset_alias, page, limit: activePageSize, search: debounced, filters: filtersParam, sortBy, sortDir });
|
||||||
if (live) setResp(r);
|
if (live) setResp(r);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
||||||
@ -119,7 +120,7 @@ export function RecordView({
|
|||||||
return () => {
|
return () => {
|
||||||
live = false;
|
live = false;
|
||||||
};
|
};
|
||||||
}, [client, rvUid, preset_alias, page, pageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]);
|
}, [client, rvUid, preset_alias, page, activePageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]);
|
||||||
|
|
||||||
const fields: RecordViewField[] = useMemo(() => {
|
const fields: RecordViewField[] = useMemo(() => {
|
||||||
const all = resp?.config.fields ?? [];
|
const all = resp?.config.fields ?? [];
|
||||||
@ -317,7 +318,16 @@ export function RecordView({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Pagination page={page} pageSize={pageSize} total={total} onPage={setPage} />
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
pageSize={activePageSize}
|
||||||
|
total={total}
|
||||||
|
onPage={setPage}
|
||||||
|
onPageSizeChange={(size) => {
|
||||||
|
setActivePageSize(size);
|
||||||
|
setPage(1); // Reset to page 1 when changing page size
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user