From 02316e7f8650b083585f7bd432289ae33e347229 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 02:41:40 +0300 Subject: [PATCH 1/7] feat: Implement pagination and infinite scroll functionality in DataTable component --- src/api/backend/types.ts | 10 +++++++ src/components/DataTable.tsx | 16 +++++++++-- src/components/InfiniteScrollTrigger.tsx | 32 +++++++++++++++++++++ src/components/index.ts | 1 + src/components/ui/index.ts | 1 + src/hooks/index.ts | 1 + src/hooks/useIsInView.ts | 36 ++++++++++++++++++++++++ 7 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/components/InfiniteScrollTrigger.tsx create mode 100644 src/hooks/useIsInView.ts diff --git a/src/api/backend/types.ts b/src/api/backend/types.ts index 7fb7a7f..befe043 100644 --- a/src/api/backend/types.ts +++ b/src/api/backend/types.ts @@ -56,6 +56,16 @@ export type VerifyOTPResponse = { export type Locale = (typeof locales)[number]; +export interface Pagination { + data: T[]; + meta: { itemsPerPage: number; totalItems: number; currentPage: number; totalPages: number }; +} + +export interface PaginationParams { + page?: number; + limit?: number; +} + export interface BasicUser { id: number; name: string; diff --git a/src/components/DataTable.tsx b/src/components/DataTable.tsx index 49580cf..7842f20 100644 --- a/src/components/DataTable.tsx +++ b/src/components/DataTable.tsx @@ -1,15 +1,19 @@ 'use client'; import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { cn } from '@/lib/utils'; import { useRouter } from 'next/navigation'; +import { InfiniteScrollTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components'; +import { cn } from '@/lib/utils'; + interface DataTableProps { columns: ColumnDef[]; data: TData[]; placeholder?: React.ReactNode; className?: string; getRowHref?: (row: TData) => string; + onLoadMore?: () => void; + hasMore?: boolean; + isLoadingMore?: boolean; } export function DataTable({ @@ -18,6 +22,9 @@ export function DataTable({ placeholder, className, getRowHref, + onLoadMore, + hasMore = false, + isLoadingMore = false, }: DataTableProps) { const router = useRouter(); const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }); @@ -73,6 +80,11 @@ export function DataTable({ )} + + {hasRows && hasMore && onLoadMore && ( + + )} + {!hasRows && (
{placeholder || 'No Results Found.'} diff --git a/src/components/InfiniteScrollTrigger.tsx b/src/components/InfiniteScrollTrigger.tsx new file mode 100644 index 0000000..f8e89cd --- /dev/null +++ b/src/components/InfiniteScrollTrigger.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useEffect } from 'react'; +import { useIsInView } from '@/hooks/useIsInView'; + +interface InfiniteScrollTriggerProps { + onLoadMore: () => void; + hasMore: boolean; + isLoading: boolean; + root?: Element | null; + rootMargin?: string; +} + +export function InfiniteScrollTrigger({ + onLoadMore, + hasMore, + isLoading, + root = null, + rootMargin = '200px', +}: InfiniteScrollTriggerProps) { + const [ref, isInView] = useIsInView({ root, rootMargin }); + + useEffect(() => { + if (isInView && hasMore && !isLoading) { + onLoadMore(); + } + }, [isInView, hasMore, isLoading, onLoadMore]); + + if (!hasMore) return null; + + return
; +} diff --git a/src/components/index.ts b/src/components/index.ts index a68e340..90aa89d 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -37,3 +37,4 @@ export * from './discounts'; export * from './StatusBadge'; export * from './NumberInput'; export * from './ReorderedMediaUpload'; +export * from './InfiniteScrollTrigger'; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index a548187..dc25569 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -24,3 +24,4 @@ export * from './popover'; export * from './calendar'; export * from './card'; export * from './input-group'; +export * from './table'; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index eb3796d..7aa0c10 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -21,3 +21,4 @@ export * from './orders'; export * from './useSidebarStatsQuery'; export * from './settings'; export * from './discounts'; +export * from './useIsInView'; diff --git a/src/hooks/useIsInView.ts b/src/hooks/useIsInView.ts new file mode 100644 index 0000000..4153f3c --- /dev/null +++ b/src/hooks/useIsInView.ts @@ -0,0 +1,36 @@ +'use client'; + +import { useState, useRef, RefObject, useEffect } from 'react'; + +interface UseIsInViewOptions extends IntersectionObserverInit { + isInView?: boolean; +} + +export const useIsInView = ( + options: UseIsInViewOptions = {}, +): [RefObject, boolean] => { + const { threshold = 0, root = null, rootMargin = '0px', isInView: initialIsInView = false } = options; + + const ref = useRef(null); + const [isInView, setIsInView] = useState(initialIsInView); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + const observer = new IntersectionObserver( + ([entry]) => { + setIsInView(entry.isIntersecting); + }, + { threshold, root, rootMargin }, + ); + + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [threshold, root, rootMargin]); + + return [ref, isInView]; +}; From ef1ca78252ec8edbfb4bf127f63135d381a10aee Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 03:37:19 +0300 Subject: [PATCH 2/7] feat: Refactor user queries to use infinite scrolling and update related components --- src/app/(hydration)/(base)/users/page.tsx | 9 +- src/components/users/UserSelectModal.tsx | 131 ++++++++++++---------- src/components/users/UsersList.tsx | 11 +- src/hooks/users/index.ts | 2 +- src/hooks/users/useCreateUserMutation.ts | 2 +- src/hooks/users/useUpdateUserMutation.ts | 2 +- src/hooks/users/useUsersInfiniteQuery.ts | 39 +++++++ src/hooks/users/useUsersQuery.ts | 31 ----- 8 files changed, 129 insertions(+), 98 deletions(-) create mode 100644 src/hooks/users/useUsersInfiniteQuery.ts delete mode 100644 src/hooks/users/useUsersQuery.ts diff --git a/src/app/(hydration)/(base)/users/page.tsx b/src/app/(hydration)/(base)/users/page.tsx index 554fd73..b7fdea5 100644 --- a/src/app/(hydration)/(base)/users/page.tsx +++ b/src/app/(hydration)/(base)/users/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import { Metadata } from 'next'; import { SearchUsersSchema } from '@/api'; -import { useUsersQuery } from '@/hooks'; +import { useUsersInfiniteQuery } from '@/hooks'; import { Searchbar, UsersCTA, UsersList } from '@/components'; import { PagePropsWithSearchParams } from '@/app/types'; @@ -14,9 +14,10 @@ export default async function Users(props: PagePropsWithSearchParams extends Disclosure { mode: M; @@ -31,6 +32,7 @@ export const UserSelectModal = ({ const isSingleMode = mode === 'single'; const [search, setSearch] = useState(); + const [listContainer, setListContainer] = useState(null); const debouncedSearch = useDebounce(search, 700); @@ -38,13 +40,15 @@ export const UserSelectModal = ({ setSearch(debouncedSearch); }, [debouncedSearch]); - const usersQuery = useUsersQuery(!!debouncedSearch ? { q: debouncedSearch } : {}, disclosure.open); - - const selectedUsersById = useMemo( - () => usersQuery.data?.filter((user) => userIds?.includes(user.id)), - [usersQuery.data, userIds], + const usersQuery = useUsersInfiniteQuery( + { limit: 10, ...(!!debouncedSearch ? { q: debouncedSearch } : {}) }, + disclosure.open, ); + const users = useMemo(() => usersQuery.data?.pages.flatMap((page) => page.data) ?? [], [usersQuery.data]); + + const selectedUsersById = useMemo(() => users.filter((user) => userIds?.includes(user.id)), [users, userIds]); + const [selectedUsers, setSelectedUsers] = useState(selectedUsersById ?? []); useEffect(() => { @@ -62,63 +66,76 @@ export const UserSelectModal = ({ className="w-full min-h-10 p-2 px-3 text-sm/relaxed md:text-base/relaxed bg-accent" /> -
+
{usersQuery.isPending ? ( ) : ( - usersQuery.data?.map((user) => { - const isSelected = selectedUsers.some(({ id }) => id === user.id); - - return ( - - ); - }) + + ); + })} + + + + {usersQuery.isFetchingNextPage && } + )}
diff --git a/src/components/users/UsersList.tsx b/src/components/users/UsersList.tsx index cf7500c..24942ef 100644 --- a/src/components/users/UsersList.tsx +++ b/src/components/users/UsersList.tsx @@ -1,7 +1,7 @@ 'use client'; import { SearchUsersSchema } from '@/api'; -import { useUsersQuery } from '@/hooks'; +import { useUsersInfiniteQuery } from '@/hooks'; import { DataTable } from '../DataTable'; import { userColumns } from './userColumns'; @@ -11,14 +11,19 @@ interface UsersListProps { } export const UsersList = ({ searchParams }: UsersListProps) => { - const usersQuery = useUsersQuery(searchParams); + const usersQuery = useUsersInfiniteQuery(searchParams); + + const users = usersQuery.data?.pages.flatMap(({ data }) => data) ?? []; return ( `/users/${id}`} + onLoadMore={usersQuery.fetchNextPage} + hasMore={usersQuery.hasNextPage} + isLoadingMore={usersQuery.isFetchingNextPage} /> ); }; diff --git a/src/hooks/users/index.ts b/src/hooks/users/index.ts index 6f244f6..2b39920 100644 --- a/src/hooks/users/index.ts +++ b/src/hooks/users/index.ts @@ -1,5 +1,5 @@ export * from './useMeQuery'; -export * from './useUsersQuery'; +export * from './useUsersInfiniteQuery'; export * from './useCreateUserMutation'; export * from './useUserQuery'; export * from './useUpdateUserMutation'; diff --git a/src/hooks/users/useCreateUserMutation.ts b/src/hooks/users/useCreateUserMutation.ts index 355ff82..7b99628 100644 --- a/src/hooks/users/useCreateUserMutation.ts +++ b/src/hooks/users/useCreateUserMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { BackendError, CreateUserSchema, SearchUser, ValidationErrors, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateUsersQuery } from './useUsersQuery'; +import { useInvalidateUsersQuery } from './useUsersInfiniteQuery'; export const useCreateUserMutation = () => { const errorHandler = useErrorHandler(); diff --git a/src/hooks/users/useUpdateUserMutation.ts b/src/hooks/users/useUpdateUserMutation.ts index d53d508..48d2713 100644 --- a/src/hooks/users/useUpdateUserMutation.ts +++ b/src/hooks/users/useUpdateUserMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { BackendError, CreateUserSchema, FullUser, ValidationErrors, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateUsersQuery } from './useUsersQuery'; +import { useInvalidateUsersQuery } from './useUsersInfiniteQuery'; import { useSetUserQueryData } from './useUserQuery'; interface UpdateUserMutationOptions extends CreateUserSchema { diff --git a/src/hooks/users/useUsersInfiniteQuery.ts b/src/hooks/users/useUsersInfiniteQuery.ts new file mode 100644 index 0000000..3807e71 --- /dev/null +++ b/src/hooks/users/useUsersInfiniteQuery.ts @@ -0,0 +1,39 @@ +import { InfiniteData, QueryFunctionContext, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError, AxiosResponse } from 'axios'; + +import { SearchUser, BackendError, gamersCoreAdmin, Pagination, PaginationParams, SearchUsersSchema } from '@/api'; + +const queryKey = (searchOptions: UsersInfiniteQueryParams = {}) => + ['users', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; + +type QueryKey = ReturnType; + +const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => + gamersCoreAdmin + .get< + Pagination, + AxiosResponse> + >('/users', { params: { page: pageParam, limit: 26, ...Object.fromEntries(paramsArr) } }) + .then((res) => res.data); + +type UsersInfiniteQueryParams = SearchUsersSchema & PaginationParams; + +export const useUsersInfiniteQuery = (searchOptions: UsersInfiniteQueryParams = {}, enabled = true) => + useInfiniteQuery, AxiosError, InfiniteData>, QueryKey>({ + queryKey: queryKey(searchOptions), + queryFn, + staleTime: 1000 * 60 * 5, // 5 minutes + enabled, + getNextPageParam: (lastPage) => + lastPage.meta.currentPage < lastPage.meta.totalPages ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 1, + }); + +export const useInvalidateUsersQuery = (searchOptions: UsersInfiniteQueryParams = {}) => { + const queryClient = useQueryClient(); + + return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); +}; + +useUsersInfiniteQuery.queryKey = queryKey; +useUsersInfiniteQuery.queryFn = queryFn; diff --git a/src/hooks/users/useUsersQuery.ts b/src/hooks/users/useUsersQuery.ts deleted file mode 100644 index 8ce9825..0000000 --- a/src/hooks/users/useUsersQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { QueryFunctionContext, useQuery, useQueryClient } from '@tanstack/react-query'; -import { AxiosError, AxiosResponse } from 'axios'; - -import { SearchUser, BackendError, SearchUsersSchema, gamersCoreAdmin } from '@/api'; - -const queryKey = (searchOptions: SearchUsersSchema = {}) => - ['users', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; - -type QueryKey = ReturnType; - -const queryFn = ({ queryKey: [, ...paramsArr] }: QueryFunctionContext) => - gamersCoreAdmin - .get>('/users', { params: Object.fromEntries(paramsArr) }) - .then((res) => res.data); - -export const useUsersQuery = (searchOptions: SearchUsersSchema = {}, enabled = true) => - useQuery, SearchUser[], QueryKey>({ - queryKey: queryKey(searchOptions), - queryFn, - staleTime: 1000 * 60 * 5, // 5 minutes - enabled, - }); - -export const useInvalidateUsersQuery = (searchOptions: SearchUsersSchema = {}) => { - const queryClient = useQueryClient(); - - return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); -}; - -useUsersQuery.queryKey = queryKey; -useUsersQuery.queryFn = queryFn; From 79d35123cc748dd9fbc930c9c5c4f38cb6834113 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 04:07:41 +0300 Subject: [PATCH 3/7] feat: Migrate orders to use infinite scrolling with updated queries and components --- src/app/(hydration)/(base)/orders/page.tsx | 9 +++-- src/components/orders/OrdersList.tsx | 11 ++++-- src/hooks/orders/index.ts | 2 +- src/hooks/orders/useAddOrderItemMutation.ts | 2 +- src/hooks/orders/useCreateOrderMutation.ts | 2 +- src/hooks/orders/useOrdersInfiniteQuery.ts | 38 +++++++++++++++++++ src/hooks/orders/useOrdersQuery.ts | 30 --------------- .../orders/useRemoveOrderItemMutation.ts | 2 +- .../orders/useRestockInventoryMutation.ts | 2 +- .../orders/useUpdateOrderItemMutation.ts | 2 +- .../useUpdateOrderPaymentStatusMutation.ts | 2 +- .../orders/useUpdateOrderStatusMutation.ts | 2 +- src/hooks/orders/useUpdateShippingMutation.ts | 2 +- 13 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 src/hooks/orders/useOrdersInfiniteQuery.ts delete mode 100644 src/hooks/orders/useOrdersQuery.ts diff --git a/src/app/(hydration)/(base)/orders/page.tsx b/src/app/(hydration)/(base)/orders/page.tsx index 1ceb85c..38bc57f 100644 --- a/src/app/(hydration)/(base)/orders/page.tsx +++ b/src/app/(hydration)/(base)/orders/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import { Metadata } from 'next'; import { SearchOrderSchema } from '@/api'; -import { useOrdersQuery } from '@/hooks'; +import { useOrdersInfiniteQuery } from '@/hooks'; import { OrdersCTA, OrdersList, Searchbar } from '@/components'; import { PagePropsWithSearchParams } from '@/app/types'; @@ -14,9 +14,10 @@ export default async function Orders(props: PagePropsWithSearchParams { - const ordersQuery = useOrdersQuery(searchParams); + const ordersQuery = useOrdersInfiniteQuery(searchParams); + + const orders = ordersQuery.data?.pages.flatMap(({ data }) => data) ?? []; return ( `/orders/${orderNumber}`} + onLoadMore={ordersQuery.fetchNextPage} + hasMore={ordersQuery.hasNextPage} + isLoadingMore={ordersQuery.isFetchingNextPage} /> ); }; diff --git a/src/hooks/orders/index.ts b/src/hooks/orders/index.ts index 9da1cf9..a0cec21 100644 --- a/src/hooks/orders/index.ts +++ b/src/hooks/orders/index.ts @@ -1,4 +1,4 @@ -export * from './useOrdersQuery'; +export * from './useOrdersInfiniteQuery'; export * from './useOrderQuery'; export * from './useUpdateOrderStatusMutation'; export * from './useUpdateOrderPaymentStatusMutation'; diff --git a/src/hooks/orders/useAddOrderItemMutation.ts b/src/hooks/orders/useAddOrderItemMutation.ts index 55a25fa..e6c605c 100644 --- a/src/hooks/orders/useAddOrderItemMutation.ts +++ b/src/hooks/orders/useAddOrderItemMutation.ts @@ -7,7 +7,7 @@ import { AddOrderItemSchema, BackendError, Order, ValidationErrors, gamersCoreAd import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; import { useInvalidateProductsQuery } from '../products'; interface AddOrderItemMutationOptions extends AddOrderItemSchema { diff --git a/src/hooks/orders/useCreateOrderMutation.ts b/src/hooks/orders/useCreateOrderMutation.ts index 6e70cd0..3fd2059 100644 --- a/src/hooks/orders/useCreateOrderMutation.ts +++ b/src/hooks/orders/useCreateOrderMutation.ts @@ -7,7 +7,7 @@ import { AddOrderItemSchema, BackendError, CreateOrderSchema, Order, ValidationE import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; import { useInvalidateProductsQuery } from '../products'; interface CreateOrderMutationData extends Omit { diff --git a/src/hooks/orders/useOrdersInfiniteQuery.ts b/src/hooks/orders/useOrdersInfiniteQuery.ts new file mode 100644 index 0000000..dee7ca7 --- /dev/null +++ b/src/hooks/orders/useOrdersInfiniteQuery.ts @@ -0,0 +1,38 @@ +import { InfiniteData, QueryFunctionContext, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError, AxiosResponse } from 'axios'; + +import { Order, BackendError, gamersCoreAdmin, Pagination, PaginationParams, SearchOrderSchema } from '@/api'; + +const queryKey = (searchOptions: OrdersInfiniteQueryParams = {}) => + ['orders', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; + +type QueryKey = ReturnType; + +const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => + gamersCoreAdmin + .get, AxiosResponse>>('/orders', { + params: { page: pageParam, limit: 18, ...Object.fromEntries(paramsArr) }, + }) + .then((res) => res.data); + +type OrdersInfiniteQueryParams = SearchOrderSchema & PaginationParams; + +export const useOrdersInfiniteQuery = (searchOptions: OrdersInfiniteQueryParams = {}, enabled = true) => + useInfiniteQuery, AxiosError, InfiniteData>, QueryKey>({ + queryKey: queryKey(searchOptions), + queryFn, + staleTime: 1000 * 60 * 5, // 5 minutes + enabled, + getNextPageParam: (lastPage) => + lastPage.meta.currentPage < lastPage.meta.totalPages ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 1, + }); + +export const useInvalidateOrdersQuery = (searchOptions: OrdersInfiniteQueryParams = {}) => { + const queryClient = useQueryClient(); + + return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); +}; + +useOrdersInfiniteQuery.queryKey = queryKey; +useOrdersInfiniteQuery.queryFn = queryFn; diff --git a/src/hooks/orders/useOrdersQuery.ts b/src/hooks/orders/useOrdersQuery.ts deleted file mode 100644 index 2b853d0..0000000 --- a/src/hooks/orders/useOrdersQuery.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { QueryFunctionContext, useQuery, useQueryClient } from '@tanstack/react-query'; -import { AxiosError, AxiosResponse } from 'axios'; - -import { BackendError, gamersCoreAdmin, Order, SearchOrderSchema } from '@/api'; - -const queryKey = (searchOptions: SearchOrderSchema = {}) => - ['orders', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; - -type QueryKey = ReturnType; - -const queryFn = ({ queryKey: [, ...paramsArr] }: QueryFunctionContext) => - gamersCoreAdmin - .get>('/orders', { params: Object.fromEntries(paramsArr) }) - .then((res) => res.data); - -export const useOrdersQuery = (searchOptions: SearchOrderSchema = {}) => - useQuery, Order[], QueryKey>({ - queryKey: queryKey(searchOptions), - queryFn, - staleTime: 1000 * 60 * 5, // 5 minutes - }); - -export const useInvalidateOrdersQuery = (searchOptions: SearchOrderSchema = {}) => { - const queryClient = useQueryClient(); - - return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); -}; - -useOrdersQuery.queryKey = queryKey; -useOrdersQuery.queryFn = queryFn; diff --git a/src/hooks/orders/useRemoveOrderItemMutation.ts b/src/hooks/orders/useRemoveOrderItemMutation.ts index 081b1b2..2169cdf 100644 --- a/src/hooks/orders/useRemoveOrderItemMutation.ts +++ b/src/hooks/orders/useRemoveOrderItemMutation.ts @@ -7,7 +7,7 @@ import { BackendError, Order, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; import { useInvalidateProductsQuery } from '../products'; interface RemoveOrderItemMutationOptions { diff --git a/src/hooks/orders/useRestockInventoryMutation.ts b/src/hooks/orders/useRestockInventoryMutation.ts index 08d78c9..821ace1 100644 --- a/src/hooks/orders/useRestockInventoryMutation.ts +++ b/src/hooks/orders/useRestockInventoryMutation.ts @@ -7,7 +7,7 @@ import { BackendError, Order, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; import { useInvalidateProductsQuery } from '../products'; export const useRestockInventoryMutation = () => { diff --git a/src/hooks/orders/useUpdateOrderItemMutation.ts b/src/hooks/orders/useUpdateOrderItemMutation.ts index 83bf5ff..da4e669 100644 --- a/src/hooks/orders/useUpdateOrderItemMutation.ts +++ b/src/hooks/orders/useUpdateOrderItemMutation.ts @@ -7,7 +7,7 @@ import { BackendError, Order, UpdateOrderItemSchema, ValidationErrors, gamersCor import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; import { useInvalidateProductsQuery } from '../products'; interface UpdateOrderItemMutationOptions extends UpdateOrderItemSchema { diff --git a/src/hooks/orders/useUpdateOrderPaymentStatusMutation.ts b/src/hooks/orders/useUpdateOrderPaymentStatusMutation.ts index 9fa18dd..f51e9ff 100644 --- a/src/hooks/orders/useUpdateOrderPaymentStatusMutation.ts +++ b/src/hooks/orders/useUpdateOrderPaymentStatusMutation.ts @@ -7,7 +7,7 @@ import { BackendError, Order, PaymentStatus, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; interface UpdateOrderPaymentStatusData { paymentStatus: PaymentStatus; diff --git a/src/hooks/orders/useUpdateOrderStatusMutation.ts b/src/hooks/orders/useUpdateOrderStatusMutation.ts index e312d80..b47f63f 100644 --- a/src/hooks/orders/useUpdateOrderStatusMutation.ts +++ b/src/hooks/orders/useUpdateOrderStatusMutation.ts @@ -7,7 +7,7 @@ import { BackendError, Order, OrderStatus, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; interface UpdateOrderStatusData { status: OrderStatus; diff --git a/src/hooks/orders/useUpdateShippingMutation.ts b/src/hooks/orders/useUpdateShippingMutation.ts index 4106f9c..853250e 100644 --- a/src/hooks/orders/useUpdateShippingMutation.ts +++ b/src/hooks/orders/useUpdateShippingMutation.ts @@ -7,7 +7,7 @@ import { BackendError, Order, OrderShippingSchema, ValidationErrors, gamersCoreA import { useErrorHandler } from '../useErrorHandler'; import { useSetOrderQueryData } from './useOrderQuery'; -import { useInvalidateOrdersQuery } from './useOrdersQuery'; +import { useInvalidateOrdersQuery } from './useOrdersInfiniteQuery'; interface UpdateOrderShippingMutationOptions extends OrderShippingSchema { orderNumber: string; From dd71a2d5f53091fee4f7eab73779511c74c0ffa0 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 04:23:33 +0300 Subject: [PATCH 4/7] feat: Refactor product queries to use infinite scrolling and update related components --- .../(base)/(store)/featured-variants/page.tsx | 8 ++- .../(base)/(store)/products/page.tsx | 9 +-- .../products/ProductVariantsModal.tsx | 66 ++++++++++++------- src/components/products/ProductsList.tsx | 11 +++- src/hooks/products/index.ts | 2 +- src/hooks/products/useAddProductMutation.ts | 2 +- .../products/useProductsInfiniteQuery.ts | 38 +++++++++++ src/hooks/products/useProductsQuery.ts | 31 --------- .../products/useRemoveProductMutation.ts | 2 +- .../products/useUpdateProductMutation.ts | 2 +- 10 files changed, 103 insertions(+), 68 deletions(-) create mode 100644 src/hooks/products/useProductsInfiniteQuery.ts delete mode 100644 src/hooks/products/useProductsQuery.ts diff --git a/src/app/(hydration)/(base)/(store)/featured-variants/page.tsx b/src/app/(hydration)/(base)/(store)/featured-variants/page.tsx index 9ea13c8..b91d5cb 100644 --- a/src/app/(hydration)/(base)/(store)/featured-variants/page.tsx +++ b/src/app/(hydration)/(base)/(store)/featured-variants/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; -import { useFeaturedVariantsQuery, useProductsQuery } from '@/hooks'; +import { useFeaturedVariantsQuery, useProductsInfiniteQuery } from '@/hooks'; import { FeaturedVariantsList } from '@/components'; export const metadata: Metadata = { title: 'Gamers Core | Featured Variants' }; @@ -11,7 +11,11 @@ export default async function FeaturedVariants() { await Promise.all([ queryClient.prefetchQuery(useFeaturedVariantsQuery), - queryClient.prefetchQuery({ queryKey: useProductsQuery.queryKey(), queryFn: useProductsQuery.queryFn }), + queryClient.prefetchInfiniteQuery({ + queryKey: useProductsInfiniteQuery.queryKey(), + queryFn: useProductsInfiniteQuery.queryFn, + initialPageParam: 1, + }), ]); return ( diff --git a/src/app/(hydration)/(base)/(store)/products/page.tsx b/src/app/(hydration)/(base)/(store)/products/page.tsx index bd621ac..e737928 100644 --- a/src/app/(hydration)/(base)/(store)/products/page.tsx +++ b/src/app/(hydration)/(base)/(store)/products/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import { Metadata } from 'next'; import { ProductsCTA, ProductsList, Searchbar, StatusFilter } from '@/components'; -import { useProductsQuery } from '@/hooks'; +import { useProductsInfiniteQuery } from '@/hooks'; import { productStatuses, SearchProductSchema } from '@/api'; import { PagePropsWithSearchParams } from '@/app/types'; @@ -14,9 +14,10 @@ export default async function Products(props: PagePropsWithSearchParams extends Disclosure { mode: M; @@ -32,6 +33,7 @@ export const ProductVariantsModal = ({ const isSingleMode = mode === 'single'; const [search, setSearch] = useState(); + const [listContainer, setListContainer] = useState(null); const debouncedSearch = useDebounce(search, 700); @@ -39,14 +41,19 @@ export const ProductVariantsModal = ({ setSearch(debouncedSearch); }, [debouncedSearch]); - const productsQuery = useProductsQuery(!!debouncedSearch ? { q: debouncedSearch } : {}, disclosure.open); + const productsQuery = useProductsInfiniteQuery( + { limit: 5, ...(!!debouncedSearch ? { q: debouncedSearch } : {}) }, + disclosure.open, + ); + + const products = useMemo(() => productsQuery.data?.pages.flatMap((page) => page.data) ?? [], [productsQuery.data]); const selectedVariantsById = useMemo( () => - productsQuery.data - ?.flatMap((product) => product.variants.map((variant) => ({ ...variant, product }))) + products + .flatMap((product) => product.variants.map((variant) => ({ ...variant, product }))) .filter((variant) => variantIds?.includes(variant.id)), - [productsQuery.data, variantIds], + [products, variantIds], ); const [selectedVariants, setSelectedVariants] = useState(selectedVariantsById ?? []); @@ -66,29 +73,40 @@ export const ProductVariantsModal = ({ className="w-full min-h-10 p-2 px-3 text-sm/relaxed md:text-base/relaxed bg-accent" /> -
+
{productsQuery.isPending ? ( ) : ( - productsQuery.data?.map((product) => ( - - setSelectedVariants( - isSingleMode - ? [variant] - : (prev) => - prev.some((v) => v.id === variant.id) - ? prev.filter((v) => v.id !== variant.id) - : [...prev, variant], - ) - } - canSelectInactive={canSelectInactive} - canSelectOutOfStock={canSelectOutOfStock} + <> + {products.map((product) => ( + + setSelectedVariants( + isSingleMode + ? [variant] + : (prev) => + prev.some((v) => v.id === variant.id) + ? prev.filter((v) => v.id !== variant.id) + : [...prev, variant], + ) + } + canSelectInactive={canSelectInactive} + canSelectOutOfStock={canSelectOutOfStock} + /> + ))} + + - )) + + {productsQuery.isFetchingNextPage && } + )}
diff --git a/src/components/products/ProductsList.tsx b/src/components/products/ProductsList.tsx index 53e35f3..91e8507 100644 --- a/src/components/products/ProductsList.tsx +++ b/src/components/products/ProductsList.tsx @@ -1,7 +1,7 @@ 'use client'; import { SearchProductSchema } from '@/api'; -import { useProductsQuery } from '@/hooks'; +import { useProductsInfiniteQuery } from '@/hooks'; import { DataTable } from '../DataTable'; import { productColumns } from './productColumns'; @@ -11,14 +11,19 @@ interface ProductsListProps { } export const ProductsList = ({ searchParams }: ProductsListProps) => { - const productsQuery = useProductsQuery(searchParams); + const productsQuery = useProductsInfiniteQuery(searchParams); + + const products = productsQuery.data?.pages.flatMap((page) => page.data) ?? []; return ( `/products/${id}`} + onLoadMore={productsQuery.fetchNextPage} + hasMore={productsQuery.hasNextPage} + isLoadingMore={productsQuery.isFetchingNextPage} /> ); }; diff --git a/src/hooks/products/index.ts b/src/hooks/products/index.ts index f32cb0d..0273a35 100644 --- a/src/hooks/products/index.ts +++ b/src/hooks/products/index.ts @@ -1,5 +1,5 @@ export * from './useAddProductMutation'; export * from './useUpdateProductMutation'; -export * from './useProductsQuery'; +export * from './useProductsInfiniteQuery'; export * from './useProductQuery'; export * from './useRemoveProductMutation'; diff --git a/src/hooks/products/useAddProductMutation.ts b/src/hooks/products/useAddProductMutation.ts index a20beef..1926907 100644 --- a/src/hooks/products/useAddProductMutation.ts +++ b/src/hooks/products/useAddProductMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { ProductSchema, BackendError, Product, ValidationErrors, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateProductsQuery } from './useProductsQuery'; +import { useInvalidateProductsQuery } from './useProductsInfiniteQuery'; interface AddProductMutationData extends Omit { mediaIds: number[]; diff --git a/src/hooks/products/useProductsInfiniteQuery.ts b/src/hooks/products/useProductsInfiniteQuery.ts new file mode 100644 index 0000000..2a0d85b --- /dev/null +++ b/src/hooks/products/useProductsInfiniteQuery.ts @@ -0,0 +1,38 @@ +import { InfiniteData, QueryFunctionContext, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError, AxiosResponse } from 'axios'; + +import { Product, BackendError, gamersCoreAdmin, Pagination, PaginationParams, SearchProductSchema } from '@/api'; + +const queryKey = (searchOptions: ProductsInfiniteQueryParams = {}) => + ['products', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; + +type QueryKey = ReturnType; + +const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => + gamersCoreAdmin + .get, AxiosResponse>>('/products', { + params: { page: pageParam, limit: 15, ...Object.fromEntries(paramsArr) }, + }) + .then((res) => res.data); + +type ProductsInfiniteQueryParams = SearchProductSchema & PaginationParams; + +export const useProductsInfiniteQuery = (searchOptions: ProductsInfiniteQueryParams = {}, enabled = true) => + useInfiniteQuery, AxiosError, InfiniteData>, QueryKey>({ + queryKey: queryKey(searchOptions), + queryFn, + staleTime: 1000 * 60 * 5, // 5 minutes + enabled, + getNextPageParam: (lastPage) => + lastPage.meta.currentPage < lastPage.meta.totalPages ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 1, + }); + +export const useInvalidateProductsQuery = (searchOptions: ProductsInfiniteQueryParams = {}) => { + const queryClient = useQueryClient(); + + return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); +}; + +useProductsInfiniteQuery.queryKey = queryKey; +useProductsInfiniteQuery.queryFn = queryFn; diff --git a/src/hooks/products/useProductsQuery.ts b/src/hooks/products/useProductsQuery.ts deleted file mode 100644 index 2125b64..0000000 --- a/src/hooks/products/useProductsQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { QueryFunctionContext, useQuery, useQueryClient } from '@tanstack/react-query'; -import { AxiosError, AxiosResponse } from 'axios'; - -import { BackendError, gamersCoreAdmin, Product, SearchProductSchema } from '@/api'; - -const queryKey = (searchOptions: SearchProductSchema = {}) => - ['products', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; - -type QueryKey = ReturnType; - -const queryFn = ({ queryKey: [, ...paramsArr] }: QueryFunctionContext) => - gamersCoreAdmin - .get>('/products', { params: Object.fromEntries(paramsArr) }) - .then((res) => res.data); - -export const useProductsQuery = (searchOptions: SearchProductSchema = {}, enabled = true) => - useQuery, Product[], QueryKey>({ - queryKey: queryKey(searchOptions), - queryFn, - staleTime: 1000 * 60 * 5, // 5 minutes - enabled, - }); - -export const useInvalidateProductsQuery = (searchOptions: SearchProductSchema = {}) => { - const queryClient = useQueryClient(); - - return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); -}; - -useProductsQuery.queryKey = queryKey; -useProductsQuery.queryFn = queryFn; diff --git a/src/hooks/products/useRemoveProductMutation.ts b/src/hooks/products/useRemoveProductMutation.ts index 72d6b94..92a80f9 100644 --- a/src/hooks/products/useRemoveProductMutation.ts +++ b/src/hooks/products/useRemoveProductMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { BackendError, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateProductsQuery } from './useProductsQuery'; +import { useInvalidateProductsQuery } from './useProductsInfiniteQuery'; export const useRemoveProductMutation = () => { const errorHandler = useErrorHandler(); diff --git a/src/hooks/products/useUpdateProductMutation.ts b/src/hooks/products/useUpdateProductMutation.ts index b9a45eb..6a2dcf8 100644 --- a/src/hooks/products/useUpdateProductMutation.ts +++ b/src/hooks/products/useUpdateProductMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { ProductSchema, BackendError, Product, ValidationErrors, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateProductsQuery } from './useProductsQuery'; +import { useInvalidateProductsQuery } from './useProductsInfiniteQuery'; import { useSetProductData } from './useProductQuery'; interface UpdateProductMutationOptions extends ProductSchema { From 62c4da44a176f6755e68601e7a5c996e523468c2 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 04:25:01 +0300 Subject: [PATCH 5/7] feat: Refactor discounts to use infinite scrolling with updated queries and components --- src/app/(hydration)/(base)/discounts/page.tsx | 9 +++-- src/components/discounts/DiscountsList.tsx | 11 ++++-- src/hooks/discounts/index.ts | 2 +- src/hooks/discounts/useAddDiscountMutation.ts | 2 +- .../discounts/useDiscountsInfiniteQuery.ts | 38 +++++++++++++++++++ src/hooks/discounts/useDiscountsQuery.ts | 31 --------------- .../discounts/useRemoveDiscountMutation.ts | 2 +- .../discounts/useUpdateDiscountMutation.ts | 2 +- 8 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 src/hooks/discounts/useDiscountsInfiniteQuery.ts delete mode 100644 src/hooks/discounts/useDiscountsQuery.ts diff --git a/src/app/(hydration)/(base)/discounts/page.tsx b/src/app/(hydration)/(base)/discounts/page.tsx index 28e2c8b..83aaacc 100644 --- a/src/app/(hydration)/(base)/discounts/page.tsx +++ b/src/app/(hydration)/(base)/discounts/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import { Metadata } from 'next'; import { SearchDiscountSchema } from '@/api'; -import { useDiscountsQuery } from '@/hooks'; +import { useDiscountsInfiniteQuery } from '@/hooks'; import { DiscountsCTA, DiscountsList, Searchbar } from '@/components'; import { PagePropsWithSearchParams } from '@/app/types'; @@ -14,9 +14,10 @@ export default async function Discounts(props: PagePropsWithSearchParams { - const discountsQuery = useDiscountsQuery(searchParams); + const discountsQuery = useDiscountsInfiniteQuery(searchParams); + + const discounts = discountsQuery.data?.pages.flatMap((page) => page.data) ?? []; return ( `/discounts/${id}`} + onLoadMore={discountsQuery.fetchNextPage} + hasMore={discountsQuery.hasNextPage} + isLoadingMore={discountsQuery.isFetchingNextPage} /> ); }; diff --git a/src/hooks/discounts/index.ts b/src/hooks/discounts/index.ts index 0912ff8..5f6aa98 100644 --- a/src/hooks/discounts/index.ts +++ b/src/hooks/discounts/index.ts @@ -1,4 +1,4 @@ -export * from './useDiscountsQuery'; +export * from './useDiscountsInfiniteQuery'; export * from './useDiscountQuery'; export * from './useAddDiscountMutation'; export * from './useUpdateDiscountMutation'; diff --git a/src/hooks/discounts/useAddDiscountMutation.ts b/src/hooks/discounts/useAddDiscountMutation.ts index a0ddd2f..b6a300e 100644 --- a/src/hooks/discounts/useAddDiscountMutation.ts +++ b/src/hooks/discounts/useAddDiscountMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { DiscountSchema, BackendError, Discount, ValidationErrors, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateDiscountsQuery } from './useDiscountsQuery'; +import { useInvalidateDiscountsQuery } from './useDiscountsInfiniteQuery'; interface AddDiscountMutationOptions extends Omit< DiscountSchema, diff --git a/src/hooks/discounts/useDiscountsInfiniteQuery.ts b/src/hooks/discounts/useDiscountsInfiniteQuery.ts new file mode 100644 index 0000000..9267c9a --- /dev/null +++ b/src/hooks/discounts/useDiscountsInfiniteQuery.ts @@ -0,0 +1,38 @@ +import { InfiniteData, QueryFunctionContext, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError, AxiosResponse } from 'axios'; + +import { Discount, BackendError, gamersCoreAdmin, Pagination, PaginationParams, SearchDiscountSchema } from '@/api'; + +const queryKey = (searchOptions: DiscountsInfiniteQueryParams = {}) => + ['discounts', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; + +type QueryKey = ReturnType; + +const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => + gamersCoreAdmin + .get, AxiosResponse>>('/discounts', { + params: { page: pageParam, limit: 26, ...Object.fromEntries(paramsArr) }, + }) + .then((res) => res.data); + +type DiscountsInfiniteQueryParams = SearchDiscountSchema & PaginationParams; + +export const useDiscountsInfiniteQuery = (searchOptions: DiscountsInfiniteQueryParams = {}, enabled = true) => + useInfiniteQuery, AxiosError, InfiniteData>, QueryKey>({ + queryKey: queryKey(searchOptions), + queryFn, + staleTime: 1000 * 60 * 5, // 5 minutes + enabled, + getNextPageParam: (lastPage) => + lastPage.meta.currentPage < lastPage.meta.totalPages ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 1, + }); + +export const useInvalidateDiscountsQuery = (searchOptions: DiscountsInfiniteQueryParams = {}) => { + const queryClient = useQueryClient(); + + return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); +}; + +useDiscountsInfiniteQuery.queryKey = queryKey; +useDiscountsInfiniteQuery.queryFn = queryFn; diff --git a/src/hooks/discounts/useDiscountsQuery.ts b/src/hooks/discounts/useDiscountsQuery.ts deleted file mode 100644 index 5a419b1..0000000 --- a/src/hooks/discounts/useDiscountsQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { QueryFunctionContext, useQuery, useQueryClient } from '@tanstack/react-query'; -import { AxiosError, AxiosResponse } from 'axios'; - -import { BackendError, gamersCoreAdmin, Discount, SearchDiscountSchema } from '@/api'; - -const queryKey = (searchOptions: SearchDiscountSchema = {}) => - ['discounts', ...Object.entries(searchOptions).sort(([a], [b]) => a.localeCompare(b))] as const; - -type QueryKey = ReturnType; - -const queryFn = ({ queryKey: [, ...paramsArr] }: QueryFunctionContext) => - gamersCoreAdmin - .get>('/discounts', { params: Object.fromEntries(paramsArr) }) - .then((res) => res.data); - -export const useDiscountsQuery = (searchOptions: SearchDiscountSchema = {}, enabled = true) => - useQuery, Discount[], QueryKey>({ - queryKey: queryKey(searchOptions), - queryFn, - staleTime: 1000 * 60 * 5, // 5 minutes - enabled, - }); - -export const useInvalidateDiscountsQuery = (searchOptions: SearchDiscountSchema = {}) => { - const queryClient = useQueryClient(); - - return () => queryClient.invalidateQueries({ queryKey: queryKey(searchOptions) }); -}; - -useDiscountsQuery.queryKey = queryKey; -useDiscountsQuery.queryFn = queryFn; diff --git a/src/hooks/discounts/useRemoveDiscountMutation.ts b/src/hooks/discounts/useRemoveDiscountMutation.ts index 8f318b6..2a1dc31 100644 --- a/src/hooks/discounts/useRemoveDiscountMutation.ts +++ b/src/hooks/discounts/useRemoveDiscountMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { BackendError, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateDiscountsQuery } from './useDiscountsQuery'; +import { useInvalidateDiscountsQuery } from './useDiscountsInfiniteQuery'; export const useRemoveDiscountMutation = () => { const errorHandler = useErrorHandler(); diff --git a/src/hooks/discounts/useUpdateDiscountMutation.ts b/src/hooks/discounts/useUpdateDiscountMutation.ts index dcc2840..4a8b2fc 100644 --- a/src/hooks/discounts/useUpdateDiscountMutation.ts +++ b/src/hooks/discounts/useUpdateDiscountMutation.ts @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import { DiscountSchema, BackendError, Discount, ValidationErrors, gamersCoreAdmin } from '@/api'; import { useErrorHandler } from '../useErrorHandler'; -import { useInvalidateDiscountsQuery } from './useDiscountsQuery'; +import { useInvalidateDiscountsQuery } from './useDiscountsInfiniteQuery'; import { useSetDiscountData } from './useDiscountQuery'; interface UpdateDiscountMutationParams extends DiscountSchema { From c9f48c19ba3e25d0b491f18546fe77348a2892ba Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 04:44:48 +0300 Subject: [PATCH 6/7] feat: Refactor InfiniteScrollTrigger to improve loading logic and optimize imports --- src/components/InfiniteScrollTrigger.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/InfiniteScrollTrigger.tsx b/src/components/InfiniteScrollTrigger.tsx index f8e89cd..3ec5e0b 100644 --- a/src/components/InfiniteScrollTrigger.tsx +++ b/src/components/InfiniteScrollTrigger.tsx @@ -1,7 +1,8 @@ 'use client'; -import { useEffect } from 'react'; -import { useIsInView } from '@/hooks/useIsInView'; +import { useEffect, useRef } from 'react'; + +import { useIsInView } from '@/hooks'; interface InfiniteScrollTriggerProps { onLoadMore: () => void; @@ -20,13 +21,21 @@ export function InfiniteScrollTrigger({ }: InfiniteScrollTriggerProps) { const [ref, isInView] = useIsInView({ root, rootMargin }); + const hasTriggeredRef = useRef(false); + useEffect(() => { - if (isInView && hasMore && !isLoading) { + if (!isInView) { + hasTriggeredRef.current = false; + return; + } + + if (hasMore && !isLoading && !hasTriggeredRef.current) { + hasTriggeredRef.current = true; onLoadMore(); } }, [isInView, hasMore, isLoading, onLoadMore]); if (!hasMore) return null; - return
; + return
; } From 1ad16440ccd41125e330206f23c34045eeb0fb96 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Sun, 6 Sep 2026 20:01:12 +0300 Subject: [PATCH 7/7] feat: Update pagination parameters to improve query structure across discounts, orders, products, and users --- src/api/backend/types.ts | 1 - src/components/DataTable.tsx | 4 +++- src/hooks/discounts/useDiscountsInfiniteQuery.ts | 2 +- src/hooks/orders/useOrdersInfiniteQuery.ts | 2 +- src/hooks/products/useProductsInfiniteQuery.ts | 2 +- src/hooks/users/useUsersInfiniteQuery.ts | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/api/backend/types.ts b/src/api/backend/types.ts index befe043..14caf5a 100644 --- a/src/api/backend/types.ts +++ b/src/api/backend/types.ts @@ -62,7 +62,6 @@ export interface Pagination { } export interface PaginationParams { - page?: number; limit?: number; } diff --git a/src/components/DataTable.tsx b/src/components/DataTable.tsx index 7842f20..4a052d6 100644 --- a/src/components/DataTable.tsx +++ b/src/components/DataTable.tsx @@ -2,9 +2,11 @@ import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; -import { InfiniteScrollTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'; import { cn } from '@/lib/utils'; +import { InfiniteScrollTrigger } from './InfiniteScrollTrigger'; + interface DataTableProps { columns: ColumnDef[]; data: TData[]; diff --git a/src/hooks/discounts/useDiscountsInfiniteQuery.ts b/src/hooks/discounts/useDiscountsInfiniteQuery.ts index 9267c9a..1bde402 100644 --- a/src/hooks/discounts/useDiscountsInfiniteQuery.ts +++ b/src/hooks/discounts/useDiscountsInfiniteQuery.ts @@ -11,7 +11,7 @@ type QueryKey = ReturnType; const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => gamersCoreAdmin .get, AxiosResponse>>('/discounts', { - params: { page: pageParam, limit: 26, ...Object.fromEntries(paramsArr) }, + params: { limit: 26, ...Object.fromEntries(paramsArr), page: pageParam }, }) .then((res) => res.data); diff --git a/src/hooks/orders/useOrdersInfiniteQuery.ts b/src/hooks/orders/useOrdersInfiniteQuery.ts index dee7ca7..f0807e6 100644 --- a/src/hooks/orders/useOrdersInfiniteQuery.ts +++ b/src/hooks/orders/useOrdersInfiniteQuery.ts @@ -11,7 +11,7 @@ type QueryKey = ReturnType; const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => gamersCoreAdmin .get, AxiosResponse>>('/orders', { - params: { page: pageParam, limit: 18, ...Object.fromEntries(paramsArr) }, + params: { limit: 18, ...Object.fromEntries(paramsArr), page: pageParam }, }) .then((res) => res.data); diff --git a/src/hooks/products/useProductsInfiniteQuery.ts b/src/hooks/products/useProductsInfiniteQuery.ts index 2a0d85b..c991cac 100644 --- a/src/hooks/products/useProductsInfiniteQuery.ts +++ b/src/hooks/products/useProductsInfiniteQuery.ts @@ -11,7 +11,7 @@ type QueryKey = ReturnType; const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext) => gamersCoreAdmin .get, AxiosResponse>>('/products', { - params: { page: pageParam, limit: 15, ...Object.fromEntries(paramsArr) }, + params: { limit: 15, ...Object.fromEntries(paramsArr), page: pageParam }, }) .then((res) => res.data); diff --git a/src/hooks/users/useUsersInfiniteQuery.ts b/src/hooks/users/useUsersInfiniteQuery.ts index 3807e71..8dcc725 100644 --- a/src/hooks/users/useUsersInfiniteQuery.ts +++ b/src/hooks/users/useUsersInfiniteQuery.ts @@ -13,7 +13,7 @@ const queryFn = ({ queryKey: [, ...paramsArr], pageParam }: QueryFunctionContext .get< Pagination, AxiosResponse> - >('/users', { params: { page: pageParam, limit: 26, ...Object.fromEntries(paramsArr) } }) + >('/users', { params: { limit: 26, ...Object.fromEntries(paramsArr), page: pageParam } }) .then((res) => res.data); type UsersInfiniteQueryParams = SearchUsersSchema & PaginationParams;