> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bigcommerce.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bigcommerce.com/_mcp/server.

# 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:

```shell showLineNumbers={false}
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.

```shell showLineNumbers={false}
degit https://github.com/bigcommerce-edu/lab-b2b-buyer-portal.git#e-comp-pre <directory-name>
```

2. **Install** dependencies.

```shell showLineNumbers={false}
cd <directory-name>
yarn install
```

3. **Run** the dev server.

```shell showLineNumbers={false}
yarn dev
```

> **Info**
>
> **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](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/tree/e-comp-post)

## 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](/_fern-img/311353b127acdf43d7b7bef477ccc909802ed903cd9f8365c9b4471e680d90c3.webp)

## Step 1: Add the Identity component

> **Info**
>
> 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`**

```tsx showLineNumbers={false} title="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`**

```tsx {1, 16} showLineNumbers={false} title="Overview/index.tsx"
import Identity from "./components/Identity";

...

export default function Overview() {
  ...

  return (
    <>
      <Grid
        ...
      >
        <Grid
          ...
        >
          <Identity />
        </Grid>

        {getOrderPermission && (
        ...
        )}
      </Grid>
    </>
  );
}
```

3. **Open** `pages/Overview/components/Identity.tsx` and **return** JSX with a `Box` component.

**`Identity.tsx`**

```tsx {6-27} showLineNumbers={false} title="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](https://v5.mui.com/material-ui/react-box/) 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](https://v5.mui.com/material-ui/customization/how-to-customize/#1-one-off-customization) 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](https://v5.mui.com/material-ui/customization/default-theme/) for the available schema.

In addition to the `sx` pattern, `Box` supports all [MUI System props](https://v5.mui.com/system/properties/).

### Adding *Grid* and *Card*

4. **Replace** the "TODO" text with JSX as shown.

**`Identity.tsx`**

```tsx {8-36} showLineNumbers={false} title="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`](https://v5.mui.com/material-ui/react-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`](https://v5.mui.com/material-ui/react-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](/_fern-img/29fb2d6135cd0e430886c31ab4def04c795be0213e875082f2b2550e34c2a4f3.webp)

Try resizing your browser window to observe the responsive behavior.

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-comp-01-pre...e-comp-01-post?diff=split)

## 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`**

```tsx {1-6} showLineNumbers={false} title="Identity.tsx"
const IdentityCard = styled(Card)(({ theme }) => ({
  backgroundColor: theme.palette.secondary.light,
  padding: '20px',
  borderRadius: '30px',
  textAlign: 'center',
}));

export default function Identity() {
  ...
}
```

2. **Replace** `Card` with `IdentityCard`.

**`Identity.tsx`**

```tsx {12, 14, 17, 19, 22, 24} showLineNumbers={false} title="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](https://v5.mui.com/system/styled/) 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.

> **Info**
>
> 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.

3. **Replace** the existing content of each card as shown.

**`Identity.tsx`**

```tsx {9-10, 18-19, 27-28} showLineNumbers={false} title="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](https://v5.mui.com/material-ui/react-typography/) 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](/_fern-img/e574ec126b1c48c0d7fc491fc53f00deee24caf082c563ab5bd352c08fec36e5.webp)

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-comp-02-pre...e-comp-02-post?diff=split)

## 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`**

```tsx {2, 20} showLineNumbers={false} title="Overview/index.tsx"
import Identity from "./components/Identity";
import RecentOrders from "./components/RecentOrders";

...

export default function Overview() {
  ...

  return (
    <>
      <Grid
        ...
      >
        ...

        {getOrderPermission && (
        <Grid
          ...
        >
          <RecentOrders />
        </Grid>
        )}
      </Grid>
    </>
  );
}
```

2. Now that we're no longer using `Button`, `HeadlessRoutes`, and `useNavigate` directly in this component, **remove** them.
   Also **remove** the `const navigate = useNavigate();` line from the component body.

**`pages/Overview/index.tsx`**

```tsx {2, 9-10,15} showLineNumbers={false} title="pages/Overview/index.tsx"
import {
  // Remove `Button` here
  Grid,
  Accordion,
  AccordionSummary,
  AccordionDetails,
  Typography,
} from "@mui/material";
// Remove the `useNavigate` import line
// Remove the `HeadlessRoutes` import line
...

export default function Overview() {
  const b3Lang = useB3Lang();
  // Remove `const navigate = useNavigate();` line from the component body.

  ...
}
```

> **Warning**
>
> Removing this unused code is important to avoid build errors later on!

3. **Open** `pages/Overview/components/RecentOrders.tsx`. **Add** the returned JSX as shown.

**`RecentOrders.tsx`**

```tsx {4-22} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
}: OrdersProps) {
  ...

  return (
    <B3Spin isSpinning={false}>
      <OverviewCard>
        <CardContent>
          <B3Table
            tableFixed={true}
            columnItems={orderColumns}
            listItems={orders}
            tableKey="orderId"
            showPagination={false}
            onClickRow={(item) => {
              navigate(`/orderDetail/${item.orderId}`);
            }}
          />
          <Button onClick={() => navigate(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 `navigate` here with a manual path based on `item.orderId`, since no constant exists for an individual order's detail page.
  * `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`.

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

5. **Add** the logic shown before the returned JSX.

**`RecentOrders.tsx`**

```tsx {5-32} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
}: 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:

```ts showLineNumbers={false}
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](/_fern-img/d72d860c36fc08fcbb83fd071c846ba717a4265b0ed87998b34f5fa0a86756c1.webp)

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-comp-03-pre...e-comp-03-post?diff=split)

## 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`**

```tsx {15-24} showLineNumbers={false} title="Overview/index.tsx"
export default function Overview() {
  ...

  return (
    <>
      <Grid
        ...
      >
        ...

        {getOrderPermission && (
        <Grid
          ...
        >
          <Accordion>
            <AccordionSummary
              expandIcon={<ExpandMoreIcon />}
            >
              <Typography variant="h3">{b3Lang('overview.recentOrders')}</Typography>
            </AccordionSummary>
            <AccordionDetails>
              <RecentOrders />
            </AccordionDetails>
          </Accordion>
        </Grid>
        )}
      </Grid>
    </>
  );
}
```

[`Accordion`](https://v5.mui.com/material-ui/react-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.

2. **Add** a state value to track whether the accordion is open.

**`Overview/index.tsx`**

```tsx {4, 20, 25} showLineNumbers={false} title="Overview/index.tsx"
export default function Overview() {
  ...

  const [ordersOpen, setOrdersOpen] = useState<boolean>(false);

  ...

  return (
    <>
      <Grid
        ...
      >
        ...

        {getOrderPermission && (
        <Grid
          ...
        >
          <Accordion
            onChange={(_e, isExpanded) => setOrdersOpen(isExpanded)}
          >
            ...
            <AccordionDetails>
              <RecentOrders
                startLoad={ordersOpen}
              />
            </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.

3. **Open** `pages/Overview/components/RecentOrders.tsx` and **add** the new prop.

**`RecentOrders.tsx`**

```tsx {2, 6} showLineNumbers={false} title="RecentOrders.tsx"
interface OrdersProps {
  startLoad: boolean;
}

export default function RecentOrders({
  startLoad,
}: OrdersProps) {
  ...
}
```

4. **Update** `useEffect` to depend on this state value, and to avoid loading data if `startLoad` is `false`.

**`RecentOrders.tsx`**

```tsx {7, 10} showLineNumbers={false} title="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](/_fern-files/bigcommerce.docs.buildwithfern.com/0a332bbd87dec51d9e97f82ba5fb3b51cd4ebcc1ba5c91d2cc4237cdb542c740/assets/learn/b2b-buyer-portal/recent-orders-accordion.gif)

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-comp-04-pre...e-comp-04-post?diff=split)

## Full Exercise Code

> **Info**
>
> [**Full Exercise Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-comp-pre...e-comp-post?diff=split)

## Taking It Further

* Explore the [Material UI components documentation](https://v5.mui.com/material-ui/all-components/) 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

* [`Box` Component](https://v5.mui.com/material-ui/react-box/)
* [One-Off Styling](https://v5.mui.com/material-ui/customization/how-to-customize/#1-one-off-customization)
* [Default Theme Values](https://v5.mui.com/material-ui/customization/default-theme/)
* [MUI System Props](https://v5.mui.com/system/properties/)
* [`Grid` Component](https://v5.mui.com/material-ui/react-grid/)
* [`Card` Component](https://v5.mui.com/material-ui/react-card/)
* [`styled` Utility](https://v5.mui.com/system/styled/)
* [`Typography` Component](https://v5.mui.com/material-ui/react-typography/)
* [`Accordion` Component](https://v5.mui.com/material-ui/react-accordion/)