From 3efa54b68a09b739ae8974e0defa4595cdb043e4 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 29 Sep 2025 00:33:32 -0700 Subject: [PATCH] add additional list pagination helpers and components --- .../helpers/item-list-paginated-loader.ts | 32 ++++---- .../item-list-pagination.module.css | 9 +++ .../item-list-pagination.tsx | 36 +++++++++ .../use-item-list-pagination.ts | 23 ++++++ .../pagination/pagination.module.css | 24 ++++-- .../components/pagination/pagination.tsx | 74 +++++++++++++++---- src/shared/hooks/use-container-query.ts | 25 +++++++ 7 files changed, 188 insertions(+), 35 deletions(-) create mode 100644 src/renderer/components/item-list/item-list-pagination/item-list-pagination.module.css create mode 100644 src/renderer/components/item-list/item-list-pagination/item-list-pagination.tsx create mode 100644 src/renderer/components/item-list/item-list-pagination/use-item-list-pagination.ts create mode 100644 src/shared/hooks/use-container-query.ts diff --git a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts index 2ce5b00fa..ace8c55b7 100644 --- a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts @@ -4,15 +4,17 @@ import { queryKeys } from '/@/renderer/api/query-keys'; import { getServerById } from '/@/renderer/store'; export interface PaginatedListProps { + initialPage?: number; + itemsPerPage?: number; query: Omit; serverId: string; } interface UseItemListPaginatedLoaderProps { + currentPage: number; itemsPerPage: number; listCountQuery: UseSuspenseQueryOptions; listQueryFn: (args: { apiClientProps: any; query: any }) => Promise<{ items: unknown[] }>; - pageIndex: number; query: Record; serverId: string; } @@ -22,28 +24,30 @@ function getInitialData(itemCount: number) { } export const useItemListPaginatedLoader = ({ + currentPage, itemsPerPage = 100, listCountQuery, listQueryFn, - pageIndex, query = {}, serverId, }: UseItemListPaginatedLoaderProps) => { const { data: totalItemCount } = useSuspenseQuery(listCountQuery); + const pageCount = Math.ceil(totalItemCount / itemsPerPage); + + const fetchRange = getFetchRange(currentPage, itemsPerPage); + const startIndex = fetchRange.startIndex; + + const queryParams = { + limit: itemsPerPage, + startIndex: startIndex, + ...query, + }; + const { data } = useQuery({ gcTime: 1000 * 15, - initialData: getInitialData(totalItemCount), + placeholderData: getInitialData(itemsPerPage), queryFn: async ({ signal }) => { - const fetchRange = getFetchRange(pageIndex, itemsPerPage); - const startIndex = fetchRange.startIndex; - - const queryParams = { - limit: itemsPerPage, - startIndex: startIndex, - ...query, - }; - const result = await listQueryFn({ apiClientProps: { server: getServerById(serverId), signal }, query: queryParams, @@ -51,11 +55,11 @@ export const useItemListPaginatedLoader = ({ return result.items; }, - queryKey: queryKeys.albums.list(serverId, query), + queryKey: queryKeys.albums.list(serverId, queryParams), staleTime: 1000 * 15, }); - return { data }; + return { data, pageCount, totalItemCount }; }; const getFetchRange = (pageIndex: number, itemsPerPage: number) => { diff --git a/src/renderer/components/item-list/item-list-pagination/item-list-pagination.module.css b/src/renderer/components/item-list/item-list-pagination/item-list-pagination.module.css new file mode 100644 index 000000000..e9ee7021a --- /dev/null +++ b/src/renderer/components/item-list/item-list-pagination/item-list-pagination.module.css @@ -0,0 +1,9 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--theme-spacing-md); + width: 100%; + height: 100%; + min-height: 0; + padding: var(--theme-spacing-md); +} diff --git a/src/renderer/components/item-list/item-list-pagination/item-list-pagination.tsx b/src/renderer/components/item-list/item-list-pagination/item-list-pagination.tsx new file mode 100644 index 000000000..d0c188cb1 --- /dev/null +++ b/src/renderer/components/item-list/item-list-pagination/item-list-pagination.tsx @@ -0,0 +1,36 @@ +import { Fragment, ReactNode } from 'react'; + +import styles from './item-list-pagination.module.css'; + +import { Pagination } from '/@/shared/components/pagination/pagination'; + +interface ItemListWithPaginationProps { + children: ReactNode; + currentPage: number; + itemsPerPage: number; + onChange: (e: number) => void; + pageCount: number; + totalItemCount: number; +} + +export const ItemListWithPagination = ({ + children, + currentPage, + itemsPerPage, + onChange, + pageCount, + totalItemCount, +}: ItemListWithPaginationProps) => { + return ( +
+ {children} + +
+ ); +}; diff --git a/src/renderer/components/item-list/item-list-pagination/use-item-list-pagination.ts b/src/renderer/components/item-list/item-list-pagination/use-item-list-pagination.ts new file mode 100644 index 000000000..d7e1852c2 --- /dev/null +++ b/src/renderer/components/item-list/item-list-pagination/use-item-list-pagination.ts @@ -0,0 +1,23 @@ +import { useSearchParams } from 'react-router-dom'; + +interface UseItemListPaginationProps { + initialPage?: number; +} + +export const useItemListPagination = ({ initialPage }: UseItemListPaginationProps) => { + const [searchParams, setSearchParams] = useSearchParams(); + + const currentPage = initialPage || Number(searchParams.get('currentPage')) || 0; + + const onChange = (index: number) => { + setSearchParams( + (params) => { + params.set('currentPage', String(index)); + return params; + }, + { replace: true }, + ); + }; + + return { currentPage, onChange }; +}; diff --git a/src/shared/components/pagination/pagination.module.css b/src/shared/components/pagination/pagination.module.css index d46acc9f5..c82335264 100644 --- a/src/shared/components/pagination/pagination.module.css +++ b/src/shared/components/pagination/pagination.module.css @@ -1,14 +1,18 @@ +.root { + justify-content: center; +} + .control { - color: var(--theme-btn-default-fg); - background-color: var(--theme-btn-default-bg); + color: var(--theme-colors-foreground); + background-color: var(--theme-colors-surface); border: none; transition: background 0.2s ease-in-out, color 0.2s ease-in-out; &[data-active] { - color: var(--theme-btn-primary-fg); - background-color: var(--theme-btn-primary-bg); + color: var(--theme-colors-primary-contrast); + background-color: var(--theme-colors-primary-filled); } &[data-dots] { @@ -16,12 +20,10 @@ } &:hover { - color: var(--theme-btn-default-fg-hover); - background-color: var(--theme-btn-default-bg-hover); + background-color: lighten(var(--theme-colors-surface), 10%); &[data-active] { - color: var(--theme-btn-primary-fg-hover); - background-color: var(--theme-btn-primary-bg-hover); + background-color: darken(var(--theme-colors-primary-filled), 10%); } &[data-dots] { @@ -29,3 +31,9 @@ } } } + +.container { + display: flex; + align-items: center; + justify-content: space-between; +} diff --git a/src/shared/components/pagination/pagination.tsx b/src/shared/components/pagination/pagination.tsx index 00abcf250..9f10c6e7a 100644 --- a/src/shared/components/pagination/pagination.tsx +++ b/src/shared/components/pagination/pagination.tsx @@ -2,23 +2,71 @@ import { Pagination as MantinePagination, PaginationProps as MantinePaginationProps, } from '@mantine/core'; +import clsx from 'clsx'; +import { useRef } from 'react'; import styles from './pagination.module.css'; -interface PaginationProps extends MantinePaginationProps {} +import { Icon } from '/@/shared/components/icon/icon'; +import { Text } from '/@/shared/components/text/text'; +import { useContainerQuery } from '/@/shared/hooks/use-container-query'; + +interface PaginationProps extends MantinePaginationProps { + containerClassName?: string; + itemsPerPage: number; + totalItemCount: number; +} + +export const Pagination = ({ + classNames, + containerClassName, + itemsPerPage, + style, + totalItemCount, + ...props +}: PaginationProps) => { + const { ref: containerRef, ...containerQuery } = useContainerQuery(); + + const paginationRef = useRef(null); + + // !IMPORTANT: Mantine Pagination is 1-indexed + const currentPageIndex = props.value || 0; + const currentPageValue = currentPageIndex + 1; + + const handleChange = (e: number) => { + props.onChange?.(e - 1); + }; + + const currentPageStartIndex = itemsPerPage * currentPageIndex + 1; + const currentPageEndIndex = Math.min(currentPageValue * itemsPerPage, totalItemCount); -export const Pagination = ({ classNames, style, ...props }: PaginationProps) => { return ( - +
+ } + previousIcon={() => } + radius="md" + ref={paginationRef} + siblings={containerQuery.isXl ? 3 : containerQuery.isMd ? 2 : 1} + size="md" + style={{ + ...style, + }} + {...props} + onChange={handleChange} + value={currentPageValue} + /> + {containerQuery.isSm && totalItemCount && ( + + {currentPageStartIndex} - {currentPageEndIndex} of {totalItemCount} + + )} +
); }; diff --git a/src/shared/hooks/use-container-query.ts b/src/shared/hooks/use-container-query.ts new file mode 100644 index 000000000..d9ec53260 --- /dev/null +++ b/src/shared/hooks/use-container-query.ts @@ -0,0 +1,25 @@ +import { useElementSize } from '@mantine/hooks'; + +interface UseContainerQueryProps { + '2xl'?: number; + '3xl'?: number; + lg?: number; + md?: number; + sm?: number; + xl?: number; +} + +export const useContainerQuery = (props?: UseContainerQueryProps) => { + const { '2xl': xxl, '3xl': xxxl, lg, md, sm, xl } = props || {}; + const { height, ref, width } = useElementSize(); + + const isXs = width >= 0; + const isSm = width >= (sm || 600); + const isMd = width >= (md || 768); + const isLg = width >= (lg || 1200); + const isXl = width >= (xl || 1500); + const is2xl = width >= (xxl || 1920); + const is3xl = width >= (xxxl || 2560); + + return { height, is2xl, is3xl, isLg, isMd, isSm, isXl, isXs, ref, width }; +};