Skip to content

Commit

Permalink
Merge pull request #9 from bc-chaz/step-5-big-design
Browse files Browse the repository at this point in the history
feat(common): adds step 5, product list using BigDesign
  • Loading branch information
bc-chaz committed Mar 2, 2021
2 parents 1923901 + 738ef9b commit fc95bdd
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 56 deletions.
60 changes: 48 additions & 12 deletions components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,55 @@
import { H2, Link } from '@bigcommerce/big-design';
import { useStore } from '../lib/hooks';
import { Box, Tabs } from '@bigcommerce/big-design';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

interface HeaderProps {
title: string;
}
const TabIds = {
HOME: 'home',
PRODUCTS: 'products',
};

const TabRoutes = {
[TabIds.HOME]: '/',
[TabIds.PRODUCTS]: '/products',
};

const Header = () => {
const [activeTab, setActiveTab] = useState(TabIds.HOME);
const router = useRouter();
const { pathname } = router;

useEffect(() => {
// Check if new route matches TabRoutes
const tabKey = Object.keys(TabRoutes).find(key => TabRoutes[key] === pathname);

// Set the active tab to tabKey or set no active tab if route doesn't match (404)
setActiveTab(tabKey ?? '');

}, [pathname]);

useEffect(() => {
// Prefetch products page to reduce latency (doesn't prefetch in dev)
router.prefetch('/products');
});

const items = [
{ id: TabIds.HOME, title: 'Home' },
{ id: TabIds.PRODUCTS, title: 'Products' },
];

const handleTabClick = (tabId: string) => {
setActiveTab(tabId);

const Header = ({ title }: HeaderProps) => {
const { storeId } = useStore();
const storeLink = `https://store-${storeId}.mybigcommerce.com/manage/marketplace/apps/my-apps`;
return router.push(TabRoutes[tabId]);
};

return (
<>
<Link href={storeLink}>My Apps</Link>
<H2 marginTop="medium">{title}</H2>
</>
<Box marginBottom="xxLarge">
<Tabs
activeTab={activeTab}
items={items}
onTabClick={handleTabClick}
/>
</Box>
);
};

Expand Down
5 changes: 2 additions & 3 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NextApiRequest, NextApiResponse } from 'next';
import * as BigCommerce from 'node-bigcommerce';
import { QueryParams, SessionProps } from '../types';
import { decode, getCookie, removeCookie, setCookie } from './cookie';
import * as fire from './firebase';
import { QueryParams } from '../types';

const { AUTH_CALLBACK, CLIENT_ID, CLIENT_SECRET, DB_TYPE } = process.env;

Expand Down Expand Up @@ -42,8 +42,7 @@ export function getBCVerify({ signed_payload }: QueryParams) {
}

export async function setSession(req: NextApiRequest, res: NextApiResponse, session: SessionProps) {
const cookies = getCookie(req);
if (!cookies) await setCookie(res, session);
await setCookie(res, session);

// Store data to specified db; needed if cookies expired/ unavailable
if (DB_TYPE === 'firebase') {
Expand Down
3 changes: 2 additions & 1 deletion lib/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { parse, serialize } from 'cookie';
import * as jwt from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';
import * as fire from './firebase';
import { SessionProps } from '../types';
import * as fire from './firebase';

const { COOKIE_NAME, JWT_KEY } = process.env;
const MAX_AGE = 60 * 60 * 24; // 24 hours
Expand Down Expand Up @@ -31,6 +31,7 @@ export function parseCookies(req: NextApiRequest) {
if (req.cookies) return req.cookies; // API routes don't parse cookies

const cookie = req.headers?.cookie;

return parse(cookie || '');
}

Expand Down
7 changes: 4 additions & 3 deletions lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import firebase from 'firebase/app';
import 'firebase/firestore';
import { SessionProps, StoreData, UserData } from '../types';

// Firebase config and initialization
// Prod applications might use config file
Expand All @@ -24,10 +25,10 @@ const db = firebase.firestore();
export async function setUser({ context, user }: SessionProps) {
if (!user) return null;

const { id, username, email } = user;
const { email, id, username } = user;
const storeId = context?.split('/')[1] || '';
const ref = db.collection('users').doc(String(id));
const data = { email, storeId };
const data: UserData = { email, storeId };

if (username) {
data.username = username;
Expand All @@ -51,7 +52,7 @@ export async function setStore(session: SessionProps) {
export async function getStore() {
const doc = await db.collection('store').limit(1).get();
const [storeDoc] = doc?.docs ?? [];
const storeData = { ...storeDoc?.data(), storeId: storeDoc?.id };
const storeData: StoreData = { ...storeDoc?.data(), storeId: storeDoc?.id };

return storeDoc.exists ? storeData : null;
}
Expand Down
12 changes: 6 additions & 6 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ function fetcher(url: string) {

// Reusable SWR hooks
// https://swr.vercel.app/
export function useStore() {
export function useProducts() {
// Request is deduped and cached; Can be shared across components
const { data, error } = useSWR('/api/store', fetcher);
const { data, error } = useSWR('/api/products', fetcher);

return {
storeId: data?.storeId,
summary: data,
isError: error,
};
}

export function useProducts() {
const { data, error } = useSWR('/api/products', fetcher);
export function useProductList() {
const { data, error } = useSWR('/api/products/list', fetcher);

return {
summary: data,
list: data,
isError: error,
};
}
8 changes: 6 additions & 2 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { GlobalStyles } from '@bigcommerce/big-design';
import { Box, GlobalStyles } from '@bigcommerce/big-design';
import type { AppProps } from 'next/app';
import Header from '../components/header';

const MyApp = ({ Component, pageProps }: AppProps) => (
<>
<GlobalStyles />
<Component {...pageProps} />
<Box marginHorizontal="xxxLarge" marginVertical="xxLarge">
<Header />
<Component {...pageProps} />
</Box>
</>
);

Expand Down
19 changes: 19 additions & 0 deletions pages/api/products/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { bigcommerceClient, getSession } from '../../../lib/auth';

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

const { data } = await bigcommerce.get(`/catalog/products?${params}`);
res.status(200).json(data);
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).end(message || 'Authentication failed, please re-install');
}
}
12 changes: 0 additions & 12 deletions pages/api/store/index.ts

This file was deleted.

35 changes: 19 additions & 16 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import { Box, Flex, Panel, Text } from '@bigcommerce/big-design';
import Header from '../components/header';
import { Box, Flex, H1, H2, H4, Link, Panel } from '@bigcommerce/big-design';
import styled from 'styled-components';
import { useProducts } from '../lib/hooks';

const Index = () => {
const { summary } = useProducts();

return (
<Panel margin="xxLarge">
<Header title="Homepage" />
<Panel header="Homepage">
{summary &&
<Flex>
<Box marginRight="xLarge">
<Text>Inventory Count</Text>
<Text>{summary.inventory_count}</Text>
</Box>
<Box marginRight="xLarge">
<Text>Variant Count</Text>
<Text>{summary.variant_count}</Text>
</Box>
<Box>
<Text>Primary Category</Text>
<Text>{summary.primary_category_name}</Text>
</Box>
<StyledBox border="box" borderRadius="normal" marginRight="xLarge" padding="medium">
<H4>Inventory count</H4>
<H1 marginBottom="none">{summary.inventory_count}</H1>
</StyledBox>
<StyledBox border="box" borderRadius="normal" marginRight="xLarge" padding="medium">
<H4>Variant count</H4>
<H1 marginBottom="none">{summary.variant_count}</H1>
</StyledBox>
<StyledBox border="box" borderRadius="normal" padding="medium">
<H4>Primary category</H4>
<H1 marginBottom="none">{summary.primary_category_name}</H1>
</StyledBox>
</Flex>
}
</Panel>
);
};

const StyledBox = styled(Box)`
min-width: 10rem;
`;

export default Index;
55 changes: 55 additions & 0 deletions pages/products/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Button, Dropdown, Panel, Small, StatefulTable, Link as StyledLink } from '@bigcommerce/big-design';
import { MoreHorizIcon } from '@bigcommerce/big-design-icons';
import Link from 'next/link';
import { ReactElement } from 'react';
import { useProductList } from '../../lib/hooks';

const Products = () => {
const { list = [] } = useProductList();
const tableItems = list.map(({ id, inventory_level: stock, name, price }) => ({
id,
name,
price,
stock,
}));

const renderName = (name: string): ReactElement => (
<Link href="#">
<StyledLink>{name}</StyledLink>
</Link>
);

const renderPrice = (price: number): string => (
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price)
);

const renderStock = (stock: number): ReactElement => (stock > 0
? <Small>{stock}</Small>
: <Small bold marginBottom="none" color="danger">0</Small>
);

const renderAction = (): ReactElement => (
<Dropdown
items={[ { content: 'Edit product', onItemClick: (item) => item, hash: 'edit' } ]}
toggle={<Button iconOnly={<MoreHorizIcon color="secondary60" />} variant="subtle" />}
/>
);

return (
<Panel>
<StatefulTable
columns={[
{ header: 'Product name', hash: 'name', render: ({ name }) => renderName(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: () => renderAction() },
]}
items={tableItems}
itemName="Products"
stickyHeader
/>
</Panel>
);
};

export default Products;
2 changes: 1 addition & 1 deletion types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export interface SessionProps {
}

export interface QueryParams {
[key: string]: string;
[key: string]: string | string[];
}
11 changes: 11 additions & 0 deletions types/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface StoreData {
accessToken?: string;
scope?: string;
storeId: string;
}

export interface UserData {
email: string;
storeId: string,
username?: string;
}
1 change: 1 addition & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './auth';
export * from './firebase';

0 comments on commit fc95bdd

Please sign in to comment.