Lab - Building Blocks

Plan: B2B Developer

Lesson 14 of 25 · 30 min

Introduction

You’ve previously scaffolded a new Overview page in the Buyer Portal. In this exercise, we’ll start to flesh out the contents of this page with the components provided by Material UI and the Buyer Portal core.

In this lab, you will:

  • Use available Material UI and Buyer Portal components to build layout
  • Learn how to create reusable styled components

Setup

This exercise continues where you left off previously. Make sure the dev server is started in your Buyer Portal project:

yarn dev

If you need a fresh start, you can follow the instructions below to set up a new project complete with previous exercise code.

  1. Copy the project with the appropriate tag.
degit https://github.com/bigcommerce-edu/lab-b2b-buyer-portal.git#e-comp-pre <directory-name>
  1. Install dependencies.
cd <directory-name>
yarn install
  1. Run the dev server.
yarn dev

Remember!

Script Manager in your store control panel must contain appropriate header/footer scripts to load the Buyer Portal from your local environment on the appropriate port.

Revisit the initial setup lab for full details on setting up your environment.

Exercise Reference Copy

If it’s helpful to compare your own code as you go, you can clone or download the completed version of this labs as a reference.

Components Lab Snapshot

The Overview Page Structure

By the end of this exercise, the Overview page will be fleshed out to include:

  • An “Identity” section: This section will render information about the user, populated by their session state, using flexible Card components.
  • A “Recent Orders” table: For now, this table will be populated by mock order data. The table will be rendered in a collapsible accordion.

Overview main layout

Step 1: Add the Identity component

Remember, all file paths referenced in these exercises are relative to apps/storefront/src in your Buyer Portal project.

Boilerplate for the Identity component already exists in pages/Overview/components/Identity.tsx. Already present in this component is logic that retrieves the user’s information from the session:

Identity.tsx
export default function Identity() {
const company = useAppSelector(({ company }) => company);
const { companyInfo, customer } = company;
const { companyName } = companyInfo;
const { firstName, lastName, companyRoleName } = customer;
...
}

We’re not focused on the use of useAppSelector in this exercise, but this will make more sense when we discuss Redux.

  1. Open pages/Overview/index.tsx.
  2. Add an import of Identity and replace the current placeholder text with the component.
Overview/index.tsx
import Identity from "./components/Identity";
...
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
return (
<>
<Grid
...
>
<Grid
...
>
<Identity />
</Grid>
{allowOrders && (
...
)}
</Grid>
</>
);
}
  1. Open pages/Overview/components/Identity.tsx and return JSX with a Box component.
Identity.tsx
export default function Identity() {
...
const b3Lang = useB3Lang();
return <>
<Box
sx={{
overflowX: 'auto',
paddingX: {
xs: '10px',
lg: '60px',
},
paddingY: {
xs: '10px',
lg: '20px',
},
backgroundColor: '#fff',
borderWidth: '2px',
borderStyle: 'solid',
borderColor: 'primary.main',
borderRadius: '30px',
}}
>
TODO: Add identity cards
</Box>
</>;
}

You should see your “TODO” text rendered on the page.

Material UI Basics

For our starter content, we’re wrapping the page in a Box component from Material UI. This is one of the most generic layout components. It is rendered as a div by default, but the HTML element can be changed with the component prop.

The sx prop demonstrates a core Material UI pattern for styling single instances of components:

  • The prop accepts an object with camel-cased CSS properties.
  • The paddingX and paddingY values demonstrate the support properties provide for responsive values. Our configuration provides certain padding values for extra small (xs) screens but overrides them with different values for large (lg) screens and above.
  • sx is “theme-aware”, meaning that many properties accept strings referring to named values in the Material UI theme. The “primary.main” string refers to a value in the “palette” theme config. See the documentation for the available schema.

In addition to the sx pattern, Box supports all MUI System props.

Adding Grid and Card

  1. Replace the “TODO” text with JSX as shown.
Identity.tsx
export default function Identity() {
...
return <>
<Box
...
>
<Grid
container
spacing={3}
>
<Grid item xs={12} lg={4}>
<Card>
<CardHeader title={b3Lang('identity.user')} />
<CardContent>
{firstName} {lastName}
</CardContent>
</Card>
</Grid>
<Grid item xs={12} lg={4}>
<Card>
<CardHeader title={b3Lang('identity.company')} />
<CardContent>
{companyName}
</CardContent>
</Card>
</Grid>
<Grid item xs={12} lg={4}>
<Card>
<CardHeader title={b3Lang('identity.role')} />
<CardContent>
{companyRoleName}
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</>;
}

We’re using two additional Material UI components here:

  • Grid facilitates simple responsive grid layouts.
    • A parent Grid (with the container prop) contains multiple Grid items (with the item prop).
    • Each grid item specifies its width in a 12-column layout. Our xs value of 12 means each item spans the full width of the container by default, while the lg value overrides this with 4 (meaning three items per row) for larger screens.
  • Card is a simple content container. Note that it contains a CardHeader and CardContent.

The Overview page should now render a basic display of the user’s identity details.

Basic Identity cards

Try resizing your browser window to observe the responsive behavior.

Step 2: Enhance the Presentation

In this step, we’ll restyle the cards used to present the identity information. While we could use the sx prop for each card, we want to avoid unnecessary duplication and centralize our styles for consistency.

  1. Open pages/Overview/components/Identity.tsx and add a definition for IdentityCard as shown.
Identity.tsx
const IdentityCard = styled(Card)(({ theme }) => ({
backgroundColor: theme.palette.secondary.light,
padding: '20px',
borderRadius: '30px',
textAlign: 'center',
}));
export default function Identity() {
...
}
  1. Replace Card with IdentityCard.
Identity.tsx
export default function Identity() {
...
return <>
<Box
...
>
<Grid
...
>
<Grid ...>
<IdentityCard>
...
</IdentityCard>
</Grid>
<Grid ...>
<IdentityCard>
...
</IdentityCard>
</Grid>
<Grid ...>
<IdentityCard>
...
</IdentityCard>
</Grid>
</Grid>
</Box>
</>;
}

styled Components

The use of the styled utility follows a standard pattern used by popular libraries like Emotion and Styled Components. It decorates an existing component with additional styling, resulting in a new component that retains the features of the base component while applying the new styles.

Like the sx prop, the Material UI styled utility is theme-aware. Note that as demonstrated in our IdentityCard component, a callback wrapper takes a parameter including theme, allowing theme values to be accessed with object syntax.

Note that the Buyer Portal core also uses the styled utility from the @emotion/styled package when values from the Material UI theme aren’t necessary. Pay attention to where styled is imported from.

Icons and Typography

We’ll also apply two other enhancements: the use of icons and standard typography.

  1. Replace the existing content of each card as shown.
Identity.tsx
<Grid
container
...
>
<Grid ...>
<IdentityCard>
...
<CardContent>
<PersonIcon fontSize="large" color="primary" />
<Typography variant="body1" fontWeight="bold">{firstName} {lastName}</Typography>
</CardContent>
</IdentityCard>
</Grid>
<Grid ...>
<IdentityCard>
...
<CardContent>
<BusinessIcon fontSize="large" color="primary" />
<Typography variant="body1" fontWeight="bold">{companyName}</Typography>
</CardContent>
</IdentityCard>
</Grid>
<Grid ...>
<IdentityCard>
...
<CardContent>
<SecurityIcon fontSize="large" color="primary" />
<Typography variant="body1" fontWeight="bold">{companyRoleName}</Typography>
</CardContent>
</IdentityCard>
</Grid>
</Grid>
  • The components PersonIcon, BusinessIcon and SecurityIcon render SVG icons from the Material Icons library (via @mui/icons-material).
  • The Typography component is theme-aware, meaning the text will respect the typography schema of the Material UI theme, based on the specified props.

Your “identity” section should now reflect the enhanced styles and icons you’ve applied.

Enhanced presentation of Identity cards

Step 3: Add Recent Orders

Now we’ll turn our attention to the page’s “Recent Orders” content. As with the previous steps, we’re starting with a boilerplate RecentOrders component that is already defined.

  1. Open pages/Overview/index.tsx to add the appropriate import and render the component.
Overview/index.tsx
import Identity from "./components/Identity";
import RecentOrders from "./components/RecentOrders";
...
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
return (
<>
<Grid
...
>
...
{allowOrders && (
<Grid
...
>
<RecentOrders
setOpenPage={setOpenPage}
/>
</Grid>
)}
</Grid>
</>
);
}

Note that we’re passing the setOpenPage function down to the new component, which will handle navigation links.

  1. Now that we’re no longer using Button and HeadlessRoutes directly in this component, remove the imports.
pages/Overview/index.tsx
import {
// Remove `Button` here
Grid,
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
} from "@mui/material";
...
// Remove `HeadlessRoutes` from this import line
import { permissionLevels } from "../../../../../../constants";

Removing these unused imports is important to avoid build errors later on!

  1. Open pages/Overview/components/RecentOrders.tsx and update the component props signature to accept setOpenPage.
RecentOrders.tsx
interface OrdersProps {
setOpenPage: SetOpenPage;
}
export default function RecentOrders({
setOpenPage,
}: OrdersProps) {
...
}
  1. Add the returned JSX as shown.
RecentOrders.tsx
export default function RecentOrders({
setOpenPage,
}: OrdersProps) {
...
return (
<B3Spin isSpinning={false}>
<OverviewCard>
<CardContent>
<B3Table
tableFixed={true}
columnItems={orderColumns}
listItems={orders}
tableKey="orderId"
showPagination={false}
onClickRow={(item) => {
setOpenPage({ isOpen: true, openUrl: `/orderDetail/${item.orderId}` });
}}
/>
<Button onClick={() => setOpenPage({ isOpen: true, openUrl: HeadlessRoutes.COMPANY_ORDERS })}>{b3Lang('overview.allOrders')}</Button>
</CardContent>
</OverviewCard>
</B3Spin>
);
}

The notable new components we’re using are provided by the Buyer Portal core:

  • B3Spin displays a spinner until content is ready to be displayed. For now, isSpinning is set to false, but later we’ll wire it to React state.
  • B3Table provides sophisticated rendering for lists of records.
    • Note the onClickRow callback, which accepts one of the rendered items. We’re using setOpenPage here, but we’re using a manual path based on item.orderId instead of a constant.
    • listItems should be an array of the items to be rendered. orders is a React state value not yet being populated.
    • columnItems should be an object defining the table’s columns. We haven’t yet defined orderColumns.

Our code also includes a custom OverviewCard component, which is another simple example of styled.

  1. Add the logic shown before the returned JSX.
RecentOrders.tsx
export default function RecentOrders({
setOpenPage,
}: OrdersProps) {
...
useEffect(() => {
setOrders(mockOrders);
}, []);
const orderColumns = [
{
key: 'orderId',
title: b3Lang('orders.order'),
},
{
key: 'poNumber',
title: b3Lang('orders.poReference'),
},
{
key: 'totalIncTax',
title: b3Lang('orders.grandTotal'),
render: (item: OverviewOrder) => {
return currencyFormat(item.totalIncTax);
},
},
{
key: 'createdAt',
title: b3Lang('orders.createdOn'),
render: (item: OverviewOrder) => {
return `${displayFormat(Number(item.createdAt))}`;
},
},
];
return (
...
);
}

We’ve put a simple React side effect in place, which will execute once when the component mounts. We’re not yet worrying about fetching real order data, instead setting the orders state value with an array of mock orders.

You can inspect mockOrders in the same file to see the format of the order records. This matches the schema the real data will eventually have:

const mockOrders = [
{
orderId: '1234567890',
poNumber: '12345',
totalIncTax: 1000,
createdAt: 1761592667,
},
{
orderId: '1234567891',
poNumber: '12346',
totalIncTax: 4500,
createdAt: 1761595667,
},
];

The orderColumns array defines each column we want to display, with a key matching one of the properties in the record data. By default, each row will render the value of the specified field as simple text. For two of these columns, we’ve provided a render callback that formats the rendered value uniquely.

You should now have the “Recent Orders” table rendered on the page with mock data.

Recent Orders table

Step 4: Add an Accordion

For our last step, we will wrap the “Recent Orders” table in an accordion component, collapsed by default. We will eventually be adding other data types to this page, so we’ll allow users to expand the sections they want to view.

  1. Open pages/Overview/index.tsx and add the wrapping Accordion.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
return (
<>
<Grid
...
>
...
{allowOrders && (
<Grid
...
>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
>
<Typography variant="h3">{b3Lang('overview.recentOrders')}</Typography>
</AccordionSummary>
<AccordionDetails>
<RecentOrders
setOpenPage={setOpenPage}
/>
</AccordionDetails>
</Accordion>
</Grid>
)}
</Grid>
</>
);
}

Accordion is our new component here. Its children, AccordionSummary and AccordionDetails, are fairly self-explanatory.

Now that the orders table isn’t displayed initially, we can also defer fetching its data until the accordion is expanded. Once we’re loading data with real GraphQL requests, this will save us from unnecessary queries.

  1. Add a state value to track whether the accordion is open.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
const [ordersOpen, setOrdersOpen] = useState<boolean>(false);
...
return (
<>
<Grid
...
>
...
{allowOrders && (
<Grid
...
>
<Accordion
onChange={(_e, isExpanded) => setOrdersOpen(isExpanded)}
>
...
<AccordionDetails>
<RecentOrders
startLoad={ordersOpen}
setOpenPage={setOpenPage}
/>
</AccordionDetails>
</Accordion>
</Grid>
)}
</Grid>
</>
);
}

We’re using a change handler on Accordion to set the ordersOpen state value, and we’re passing this to a new startLoad prop on RecentOrders to let that component know the status.

  1. Open pages/Overview/components/RecentOrders.tsx and add the new prop.
RecentOrders.tsx
interface OrdersProps {
startLoad: boolean;
setOpenPage: SetOpenPage;
}
export default function RecentOrders({
startLoad,
setOpenPage,
}: OrdersProps) {
...
}
  1. Update useEffect to depend on this state value, and to avoid loading data if startLoad is false.
RecentOrders.tsx
export default function RecentOrders({
...
}: OrdersProps) {
...
useEffect(() => {
if (!startLoad) return;
setOrders(mockOrders);
}, [startLoad]);
...
}

Navigate to your Overview page and verify that the “Recent Orders” table is rendered in a working accordion.

Recent Orders accordion

Full Exercise Code

Taking It Further

  • Explore the Material UI components documentation and experiment with incorporating other components into the Overview page.
  • Create a styled version of Accordion and apply your own styles. Explore with responsive values and the use of theme values.

Resources