Skip to content

Commit

Permalink
feat(common): APPEX-78 Add product list pagination to sample app
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-zachary committed Sep 24, 2021
1 parent c8eecd8 commit 6d955f4
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 46 deletions.
27 changes: 16 additions & 11 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import useSWR from 'swr';
import { useSession } from '../context/session';
import { ErrorProps, ListItem } from '../types';
import { ErrorProps, ListItem, QueryParams } from '../types';

async function fetcher(url: string, encodedContext: string) {
const res = await fetch(`${url}?context=${encodedContext}`);
async function fetcher(url: string, query: string) {
const res = await fetch(`${url}?${query}`);

// If the status code is not in the range 200-299, throw an error
if (!res.ok) {
Expand All @@ -19,9 +19,10 @@ async function fetcher(url: string, encodedContext: string) {
// Reusable SWR hooks
// https://swr.vercel.app/
export function useProducts() {
const encodedContext = useSession()?.context;
const { context } = useSession();
const params = new URLSearchParams({ context }).toString();
// Request is deduped and cached; Can be shared across components
const { data, error } = useSWR(encodedContext ? ['/api/products', encodedContext] : null, fetcher);
const { data, error } = useSWR(context ? ['/api/products', params] : null, fetcher);

return {
summary: data,
Expand All @@ -30,24 +31,28 @@ export function useProducts() {
};
}

export function useProductList() {
const encodedContext = useSession()?.context;
export function useProductList(query?: QueryParams) {
const { context } = useSession();
const params = new URLSearchParams({ ...query, context }).toString();

// Use an array to send multiple arguments to fetcher
const { data, error, mutate: mutateList } = useSWR(encodedContext ? ['/api/products/list', encodedContext] : null, fetcher);
const { data, error, mutate: mutateList } = useSWR(context ? ['/api/products/list', params] : null, fetcher);

return {
list: data,
list: data?.data,
meta: data?.meta,
isLoading: !data && !error,
error,
mutateList,
};
}

export function useProductInfo(pid: number, list: ListItem[]) {
const encodedContext = useSession()?.context;
const { context } = useSession();
const params = new URLSearchParams({ context }).toString();
const product = list.find(item => item.id === pid);
// Conditionally fetch product if it doesn't exist in the list (e.g. deep linking)
const { data, error } = useSWR(!product && encodedContext ? [`/api/products/${pid}`, encodedContext] : null, fetcher);
const { data, error } = useSWR(!product && context ? [`/api/products/${pid}`, params] : null, fetcher);

return {
product: product ?? data,
Expand Down
11 changes: 5 additions & 6 deletions pages/api/products/list.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { URLSearchParams } from 'url';
import { bigcommerceClient, getSession } from '../../../lib/auth';

export default async function list(req: NextApiRequest, res: NextApiResponse) {
try {
const { accessToken, storeHash } = await getSession(req);
const bigcommerce = bigcommerceClient(accessToken, storeHash);
// Optional: pass in API params here
const params = [
'limit=11',
].join('&');
const { page, limit, sort, direction } = req.query;
const params = new URLSearchParams({ page, limit, ...(sort && {sort, direction}) }).toString();

const { data } = await bigcommerce.get(`/catalog/products?${params}`);
res.status(200).json(data);
const response = await bigcommerce.get(`/catalog/products?${params}`);
res.status(200).json(response);
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json({ message });
Expand Down
49 changes: 41 additions & 8 deletions pages/products/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { Button, Dropdown, Panel, Small, StatefulTable, Link as StyledLink } from '@bigcommerce/big-design';
import { Button, Dropdown, Panel, Small, Link as StyledLink, Table, TableSortDirection } from '@bigcommerce/big-design';
import { MoreHorizIcon } from '@bigcommerce/big-design-icons';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ReactElement } from 'react';
import { ReactElement, useState } from 'react';
import ErrorMessage from '../../components/error';
import Loading from '../../components/loading';
import { useProductList } from '../../lib/hooks';
import { TableItem } from '../../types';

const Products = () => {
const [itemsPerPage, setItemsPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [columnHash, setColumnHash] = useState('');
const [direction, setDirection] = useState<TableSortDirection>('ASC');
const router = useRouter();
const { error, isLoading, list = [] } = useProductList();
const { error, isLoading, list = [], meta = {} } = useProductList({
page: String(currentPage),
limit: String(itemsPerPage),
...(columnHash && { sort: columnHash }),
...(columnHash && { direction: direction.toLowerCase() }),
});
const itemsPerPageOptions = [10, 20, 50, 100];
const tableItems: TableItem[] = list.map(({ id, inventory_level: stock, name, price }) => ({
id,
name,
price,
stock,
}));

const onItemsPerPageChange = newRange => {
setCurrentPage(1);
setItemsPerPage(newRange);
};

const onSort = (newColumnHash: string, newDirection: TableSortDirection) => {
setColumnHash(newColumnHash === 'stock' ? 'inventory_level' : newColumnHash);
setDirection(newDirection);
};

const renderName = (id: number, name: string): ReactElement => (
<Link href={`/products/${id}`}>
<StyledLink>{name}</StyledLink>
Expand Down Expand Up @@ -45,15 +65,28 @@ const Products = () => {

return (
<Panel>
<StatefulTable
<Table
columns={[
{ header: 'Product name', hash: 'name', render: ({ id, name }) => renderName(id, name), sortKey: 'name' },
{ header: 'Stock', hash: 'stock', render: ({ stock }) => renderStock(stock), sortKey: 'stock' },
{ header: 'Price', hash: 'price', render: ({ price }) => renderPrice(price), sortKey: 'price' },
{ header: 'Action', hideHeader: true, hash: 'id', render: ({ id }) => renderAction(id), sortKey: 'id' },
{ header: 'Product name', hash: 'name', render: ({ id, name }) => renderName(id, name), isSortable: true },
{ header: 'Stock', hash: 'stock', render: ({ stock }) => renderStock(stock), isSortable: true },
{ header: 'Price', hash: 'price', render: ({ price }) => renderPrice(price), isSortable: true },
{ header: 'Action', hideHeader: true, hash: 'id', render: ({ id }) => renderAction(id) },
]}
items={tableItems}
itemName="Products"
pagination={{
currentPage,
totalItems: meta?.pagination?.total,
onPageChange: setCurrentPage,
itemsPerPageOptions,
onItemsPerPageChange,
itemsPerPage,
}}
sortable={{
columnHash,
direction,
onSort,
}}
stickyHeader
/>
</Panel>
Expand Down
1 change: 1 addition & 0 deletions test/mocks/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const generateList = (): TableItem[] => (

export const useProductList = jest.fn().mockImplementation(() => ({
list: generateList(),
meta: { pagination: { total: ROW_NUMBERS } }
}));

// useProductInfo Mock
Expand Down
Loading

0 comments on commit 6d955f4

Please sign in to comment.