diff --git a/src/api/backend/types.ts b/src/api/backend/types.ts index 7fb7a7f..14caf5a 100644 --- a/src/api/backend/types.ts +++ b/src/api/backend/types.ts @@ -56,6 +56,15 @@ 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 { + limit?: number; +} + export interface BasicUser { id: number; name: string; 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 { columns: ColumnDef[]; data: TData[]; placeholder?: React.ReactNode; className?: string; getRowHref?: (row: TData) => string; + onLoadMore?: () => void; + hasMore?: boolean; + isLoadingMore?: boolean; } export function DataTable({ @@ -18,6 +24,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 +82,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..3ec5e0b --- /dev/null +++ b/src/components/InfiniteScrollTrigger.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +import { useIsInView } from '@/hooks'; + +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 }); + + const hasTriggeredRef = useRef(false); + + useEffect(() => { + if (!isInView) { + hasTriggeredRef.current = false; + return; + } + + if (hasMore && !isLoading && !hasTriggeredRef.current) { + hasTriggeredRef.current = true; + onLoadMore(); + } + }, [isInView, hasMore, isLoading, onLoadMore]); + + if (!hasMore) return null; + + return
; +} diff --git a/src/components/discounts/DiscountsList.tsx b/src/components/discounts/DiscountsList.tsx index f8a16b3..d6576e8 100644 --- a/src/components/discounts/DiscountsList.tsx +++ b/src/components/discounts/DiscountsList.tsx @@ -1,7 +1,7 @@ 'use client'; import { SearchDiscountSchema } from '@/api'; -import { useDiscountsQuery } from '@/hooks'; +import { useDiscountsInfiniteQuery } from '@/hooks'; import { DataTable } from '../DataTable'; import { discountColumns } from './discountColumns'; @@ -11,14 +11,19 @@ interface DiscountsListProps { } export const DiscountsList = ({ searchParams }: DiscountsListProps) => { - 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/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/orders/OrdersList.tsx b/src/components/orders/OrdersList.tsx index cfe66c2..b131efa 100644 --- a/src/components/orders/OrdersList.tsx +++ b/src/components/orders/OrdersList.tsx @@ -1,7 +1,7 @@ 'use client'; import { SearchOrderSchema } from '@/api'; -import { useOrdersQuery } from '@/hooks'; +import { useOrdersInfiniteQuery } from '@/hooks'; import { DataTable } from '../DataTable'; import { orderColumns } from './orderColumns'; @@ -11,14 +11,19 @@ interface OrdersListProps { } export const OrdersList = ({ searchParams }: OrdersListProps) => { - 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/components/products/ProductVariantsModal.tsx b/src/components/products/ProductVariantsModal.tsx index 595dcec..1f8aa79 100644 --- a/src/components/products/ProductVariantsModal.tsx +++ b/src/components/products/ProductVariantsModal.tsx @@ -5,13 +5,14 @@ import { HugeiconsIcon } from '@hugeicons/react'; import { Check } from '@hugeicons/core-free-icons'; import { defaultLocale, Product, VariantWithProduct } from '@/api'; -import { Disclosure, useDebounce, useFormatCurrency, useProductsQuery } from '@/hooks'; +import { Disclosure, useDebounce, useFormatCurrency, useProductsInfiniteQuery } from '@/hooks'; import { cn } from '@/lib/utils'; import { Modal, ModalFooter } from '../Modal'; import { Input, Spinner } from '../ui'; import { Button } from '../Button'; import { Media } from '../Media'; +import { InfiniteScrollTrigger } from '../InfiniteScrollTrigger'; interface ProductVariantsModalProps 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/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/components/users/UserSelectModal.tsx b/src/components/users/UserSelectModal.tsx index b9c6d8d..21a8a4e 100644 --- a/src/components/users/UserSelectModal.tsx +++ b/src/components/users/UserSelectModal.tsx @@ -5,12 +5,13 @@ import { HugeiconsIcon } from '@hugeicons/react'; import { Check } from '@hugeicons/core-free-icons'; import { SearchUser } from '@/api'; -import { Disclosure, useDebounce, useUsersQuery } from '@/hooks'; +import { Disclosure, useDebounce, useUsersInfiniteQuery } from '@/hooks'; import { cn } from '@/lib/utils'; import { Modal, ModalFooter } from '../Modal'; import { Input, Spinner } from '../ui'; import { Button } from '../Button'; +import { InfiniteScrollTrigger } from '../InfiniteScrollTrigger'; interface UserSelectModalProps 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/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..1bde402 --- /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: { limit: 26, ...Object.fromEntries(paramsArr), page: pageParam }, + }) + .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 { 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/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..f0807e6 --- /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: { limit: 18, ...Object.fromEntries(paramsArr), page: pageParam }, + }) + .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; 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..c991cac --- /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: { limit: 15, ...Object.fromEntries(paramsArr), page: pageParam }, + }) + .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 { 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]; +}; 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..8dcc725 --- /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: { limit: 26, ...Object.fromEntries(paramsArr), page: pageParam } }) + .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;