Skip to content
Merged
9 changes: 9 additions & 0 deletions src/api/backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export type VerifyOTPResponse = {

export type Locale = (typeof locales)[number];

export interface Pagination<T> {
data: T[];
meta: { itemsPerPage: number; totalItems: number; currentPage: number; totalPages: number };
}

export interface PaginationParams {
limit?: number;
}

export interface BasicUser {
id: number;
name: string;
Expand Down
8 changes: 6 additions & 2 deletions src/app/(hydration)/(base)/(store)/featured-variants/page.tsx
Original file line number Diff line number Diff line change
@@ -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' };
Expand All @@ -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 (
Expand Down
9 changes: 5 additions & 4 deletions src/app/(hydration)/(base)/(store)/products/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -14,9 +14,10 @@ export default async function Products(props: PagePropsWithSearchParams<SearchPr
const queryClient = new QueryClient();

await Promise.all([
queryClient.prefetchQuery({
queryKey: useProductsQuery.queryKey(searchParams),
queryFn: useProductsQuery.queryFn,
queryClient.prefetchInfiniteQuery({
queryKey: useProductsInfiniteQuery.queryKey(searchParams),
queryFn: useProductsInfiniteQuery.queryFn,
initialPageParam: 1,
}),
]);

Expand Down
9 changes: 5 additions & 4 deletions src/app/(hydration)/(base)/discounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -14,9 +14,10 @@ export default async function Discounts(props: PagePropsWithSearchParams<SearchD
const queryClient = new QueryClient();

await Promise.all([
queryClient.prefetchQuery({
queryKey: useDiscountsQuery.queryKey(searchParams),
queryFn: useDiscountsQuery.queryFn,
queryClient.prefetchInfiniteQuery({
queryKey: useDiscountsInfiniteQuery.queryKey(searchParams),
queryFn: useDiscountsInfiniteQuery.queryFn,
initialPageParam: 1,
}),
]);

Expand Down
9 changes: 5 additions & 4 deletions src/app/(hydration)/(base)/orders/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -14,9 +14,10 @@ export default async function Orders(props: PagePropsWithSearchParams<SearchOrde
const queryClient = new QueryClient();

await Promise.all([
queryClient.prefetchQuery({
queryKey: useOrdersQuery.queryKey(searchParams),
queryFn: useOrdersQuery.queryFn,
queryClient.prefetchInfiniteQuery({
queryKey: useOrdersInfiniteQuery.queryKey(searchParams),
queryFn: useOrdersInfiniteQuery.queryFn,
initialPageParam: 1,
}),
]);

Expand Down
9 changes: 5 additions & 4 deletions src/app/(hydration)/(base)/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -14,9 +14,10 @@ export default async function Users(props: PagePropsWithSearchParams<SearchUsers
const queryClient = new QueryClient();

await Promise.all([
queryClient.prefetchQuery({
queryKey: useUsersQuery.queryKey(searchParams),
queryFn: useUsersQuery.queryFn,
queryClient.prefetchInfiniteQuery({
queryKey: useUsersInfiniteQuery.queryKey(searchParams),
queryFn: useUsersInfiniteQuery.queryFn,
initialPageParam: 1,
}),
]);

Expand Down
18 changes: 16 additions & 2 deletions src/components/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
'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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui';
import { cn } from '@/lib/utils';

import { InfiniteScrollTrigger } from './InfiniteScrollTrigger';

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
placeholder?: React.ReactNode;
className?: string;
getRowHref?: (row: TData) => string;
onLoadMore?: () => void;
hasMore?: boolean;
isLoadingMore?: boolean;
}

export function DataTable<TData, TValue>({
Expand All @@ -18,6 +24,9 @@ export function DataTable<TData, TValue>({
placeholder,
className,
getRowHref,
onLoadMore,
hasMore = false,
isLoadingMore = false,
}: DataTableProps<TData, TValue>) {
const router = useRouter();
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
Expand Down Expand Up @@ -73,6 +82,11 @@ export function DataTable<TData, TValue>({
</TableBody>
)}
</Table>

{hasRows && hasMore && onLoadMore && (
<InfiniteScrollTrigger onLoadMore={onLoadMore} hasMore={hasMore} isLoading={isLoadingMore} />
)}

{!hasRows && (
<div className="flex-1 flex items-center justify-center text-muted-foreground text-base">
{placeholder || 'No Results Found.'}
Expand Down
41 changes: 41 additions & 0 deletions src/components/InfiniteScrollTrigger.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>({ 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 <div ref={ref} aria-hidden className="size-px shrink-0" />;
}
11 changes: 8 additions & 3 deletions src/components/discounts/DiscountsList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<DataTable
data={discountsQuery.data ?? []}
data={discounts}
columns={discountColumns}
placeholder="No discounts found."
getRowHref={({ id }) => `/discounts/${id}`}
onLoadMore={discountsQuery.fetchNextPage}
hasMore={discountsQuery.hasNextPage}
isLoadingMore={discountsQuery.isFetchingNextPage}
/>
);
};
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ export * from './discounts';
export * from './StatusBadge';
export * from './NumberInput';
export * from './ReorderedMediaUpload';
export * from './InfiniteScrollTrigger';
11 changes: 8 additions & 3 deletions src/components/orders/OrdersList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<DataTable
data={ordersQuery.data ?? []}
data={orders}
columns={orderColumns}
placeholder="No orders found."
getRowHref={({ orderNumber }) => `/orders/${orderNumber}`}
onLoadMore={ordersQuery.fetchNextPage}
hasMore={ordersQuery.hasNextPage}
isLoadingMore={ordersQuery.isFetchingNextPage}
/>
);
};
66 changes: 42 additions & 24 deletions src/components/products/ProductVariantsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<M extends 'single' | 'multiple'> extends Disclosure {
mode: M;
Expand All @@ -32,21 +33,27 @@ export const ProductVariantsModal = <M extends 'single' | 'multiple'>({
const isSingleMode = mode === 'single';

const [search, setSearch] = useState<string>();
const [listContainer, setListContainer] = useState<HTMLDivElement | null>(null);

const debouncedSearch = useDebounce(search, 700);

useEffect(() => {
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<VariantWithProduct[]>(selectedVariantsById ?? []);
Expand All @@ -66,29 +73,40 @@ export const ProductVariantsModal = <M extends 'single' | 'multiple'>({
className="w-full min-h-10 p-2 px-3 text-sm/relaxed md:text-base/relaxed bg-accent"
/>

<div className="flex flex-col gap-4 overflow-y-auto">
<div ref={setListContainer} className="flex flex-col gap-4 overflow-y-auto">
{productsQuery.isPending ? (
<Spinner className="size-8 m-auto" />
) : (
productsQuery.data?.map((product) => (
<ProductCard
key={product.id}
product={product}
selectedVariants={selectedVariants}
onSelect={(variant) =>
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) => (
<ProductCard
key={product.id}
product={product}
selectedVariants={selectedVariants}
onSelect={(variant) =>
setSelectedVariants(
isSingleMode
? [variant]
: (prev) =>
prev.some((v) => v.id === variant.id)
? prev.filter((v) => v.id !== variant.id)
: [...prev, variant],
)
}
canSelectInactive={canSelectInactive}
canSelectOutOfStock={canSelectOutOfStock}
/>
))}

<InfiniteScrollTrigger
onLoadMore={productsQuery.fetchNextPage}
hasMore={!!productsQuery.hasNextPage}
isLoading={productsQuery.isFetchingNextPage}
root={listContainer}
/>
))

{productsQuery.isFetchingNextPage && <Spinner className="size-6 mx-auto my-2" />}
</>
)}
</div>

Expand Down
Loading
Loading