Use BigCommerce App Extensions to build a size chart app for Catalyst storefronts 

Expected time: 30 minutes

This tutorial will show you how to build a size chart app for BigCommerce Catalyst storefronts, using App Extensions.

By the end of this tutorial, you will have:

  • Set up a BigCommerce connection
  • Stored BigCommerce data in a Gadget database
  • Subscribed to BigCommerce webhooks and synced historical data
  • Built a BigCommerce App Extension
  • Set up a Catalyst storefront that reads size chart info from your Gadget app

Prerequisites 

Before starting, you will need:

Step 1: Create a new Gadget app and connect to BigCommerce 

You will start by creating a new Gadget app and connecting to BigCommerce.

  1. Create a new Gadget app at gadget.new, select the BigCommerce app type, and give your app a name.
  2. Click the Connect to BigCommerce button on your app's home page.
  3. Create a new BigCommerce app in the BigCommerce Developer Portal.
  4. Copy the Auth callback URL and Load callback URL from Gadget to your BigCommerce app.
  5. Select the Products Read-only and App Extensions Manage OAuth scopes and click Update & Close in the BigCommerce app.
  6. In the BigCommerce Developer Portal, click View Client ID for your new app and copy the Client ID and Client Secret to Gadget, then click Continue.
  7. In your BigCommerce sandbox store, navigate to Apps → My Draft Apps, hover over your newly added app, click Learn more.
  8. Click Install.

You now have a full-stack, single-click BigCommerce app in your store control panel. OAuth and frontend sessions are handled, and you can subscribe to BigCommerce webhooks.

Step 2: Add a product data model 

You need to store both product data and size charts created by merchants in your Gadget database. You can create data models in Gadget to store this data.

  1. Right-click on the api/models/bigcommerce directory in the Gadget file tree and select Add model.
  2. Name the model product and add the following fields and validations:
Field nameField typeValidations
namestringRequired
sizeChartjson
storebelongs toRequired
bigcommerceIdnumberUniqueness (scoped by store), Required

When a model is created and edited Gadget will automatically run the required mutations on the underlying database. A CRUD API will also be automatically generated for your model. Read more about data models in Gadget.

  1. For the store field, select the bigcommerce/store model as the parent model, so that bigcommerce/store has many bigcommerce/product.
A screenshot of the store relationship field on the bigcommerce/product model. The inverse of the relationship is a has many, so that bigcommerce/store has many bigcommerce/product records.
bigcommerceId uniqueness validation

For multi-tenant apps, you may have multiple stores whose resources have the same bigcommerceId. To avoid conflicts, you can scope the Uniqueness validation on bigcommerceId by the store relationship. This ensures that bigcommerceId is unique per store. Read more about multi-tenancy in BigCommerce apps.

Step 3: Add appExtensionId field to bigcommerce/store 

You also want to store the App Extension ID on your bigcommerce/store model so the extension can be referenced from your app.

Add the following field at api/models/bigcommerce/store/schema:

Field nameField type
appExtensionIdstring

Step 4: Subscribe to store/product webhooks 

You can use webhooks to keep your product data in Gadget up to date with data in a store.

  1. Click the + button next to api/actions and enter bigcommerce/handleProductWebhooks.js. This creates a bigcommerce namespace folder and your new action.
  2. Click the + button in the action's Triggers card and select BigCommerce.
  3. Select the store/product/created, store/product/updated, and store/product/deleted webhook scopes.
  4. (Optional) Remove the Generated API endpoint trigger from the action.

Now this action will run anytime a product is created, updated, or deleted in BigCommerce.

When a product webhook is fired, you want to call the bigcommerce/product model's actions to create, update, or delete records in the Gadget database.

Notice that the upsert meta API is used to handle store/product/updated webhooks. This is because the product may not yet exist in your database.

  1. Paste the following code in api/actions/bigcommerce/handleProductWebhooks.js:
api/actions/bigcommerce/handleProductWebhooks.js
JavaScript
1import { HandleProductWebhooksGlobalActionContext } from "gadget-server";
2
3/**
4 * @param { HandleProductWebhooksGlobalActionContext } context
5 */
6export async function run({ params, logger, api, connections, trigger }) {
7 // get the BigCommerce API client for the current store
8 const bigcommerce = connections.bigcommerce.current;
9
10 // handle store/product/deleted webhooks
11 if (trigger.scope === "store/product/deleted") {
12 await api.bigcommerce.product.deleteMany({
13 filter: {
14 bigcommerceId: {
15 // match the product id in webhook payload to what is stored in Gadget
16 equals: params.id,
17 },
18 storeId: {
19 // get the bigcommerce/store id for the record stored in Gadget
20 equals: connections.bigcommerce.currentStoreId,
21 },
22 },
23 });
24
25 // end action
26 return;
27 }
28
29 // handle store/product/created and store/product/updated webhooks
30 // fetch the product data
31 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {
32 path: {
33 product_id: params.id,
34 },
35 });
36
37 // upsert product data into the model
38 await api.bigcommerce.product.upsert({
39 bigcommerceId: product.id,
40 store: {
41 // get the bigcommerce/store id for the record stored in Gadget
42 _link: connections.bigcommerce.currentStoreId,
43 },
44 name: product.name,
45 on: ["bigcommerceId", "store"],
46 });
47}
48
49export const options = {
50 triggers: {
51 api: false,
52 bigcommerce: {
53 webhooks: [
54 "store/product/created",
55 "store/product/updated",
56 "store/product/deleted",
57 ],
58 },
59 },
60};

This will handle the product webhook topics and call the required bigcommerce/product action. The products in your Gadget database will now stay up to date with any BigCommerce stores that have installed your app.

Step 5: Sync product data and set up a BigCommerce App Extension 

You still need a way to sync existing product data into your data models. You also need to set up a BigCommerce App Extension.

You can do both of these things together in the api/models/bigcommerce/store/actions/install.js action. This action is run immediately after your app is installed on store.

Start by creating another global action to handle the data sync using Gadget's built-in background actions.

  1. Create a new file syncProducts.js in api/actions/bigcommerce and paste the following code:
api/actions/syncProducts.js
JavaScript
1import { SyncProductsGlobalActionContext } from "gadget-server";
2
3/**
4 * @param { SyncProductsGlobalActionContext } context
5 */
6export async function run({ params, logger, api, connections }) {
7 // set the batch size to 50 for bulk upsert
8 const BATCH_SIZE = 50;
9
10 const bigcommerce = await connections.bigcommerce.forStoreHash(params.storeHash);
11 // use the API client to fetch all products, and return
12 const products = await bigcommerce.v3.list(`/catalog/products`);
13 // get the id of the store record in Gadget db
14 const store = await api.bigcommerce.store.findFirst({
15 filter: {
16 storeHash: {
17 equals: params.storeHash,
18 },
19 },
20 select: {
21 id: true,
22 },
23 });
24
25 const productPayload = [];
26 // use a for await loop to iterate over the AsyncIterables, add to an array
27 for await (const product of products) {
28 productPayload.push({
29 // use bigcommerceId and store to identify unique records for upsert
30 on: ["bigcommerceId", "store"],
31 // store the BigCommerce ID
32 bigcommerceId: product.id,
33 // associate the product with the current store
34 store: {
35 _link: store.id,
36 },
37 name: product.name,
38 });
39
40 // enqueue 50 actions at a time
41 if (productPayload.length >= BATCH_SIZE) {
42 const section = productPayload.splice(0, BATCH_SIZE);
43 // bulk enqueue create action
44 await api.enqueue(api.bigcommerce.product.bulkUpsert, section, {
45 queue: { name: "product-sync" },
46 });
47
48 // delay for a second, don't exceed rate limits!
49 await new Promise((r) => setTimeout(r, 1000));
50
51 // ONLY SYNC 50 PRODUCTS FOR THE TUTORIAL
52 break;
53 }
54 }
55
56 // enqueue any remaining products
57 await api.enqueue(api.bigcommerce.product.bulkUpsert, productPayload, {
58 queue: { name: "product-sync" },
59 });
60}
61
62export const options = {
63 timeoutMS: 900000, // 15 minute timeout for the sync
64};
65
66// accept store hash as action param
67export const params = {
68 storeHash: { type: "string" },
69};

When run, this will fetch all product data from a BigCommerce store and enqueue upserts using Gadget's background actions. Background actions are used so you get the built-in retry handling to ensure data is reliably added to your database in bulk.

This sample code will break the product iteration loop after 50 products this is to limit resources used while building this tutorial. Remove the break in the snippet to sync all product data for production apps.

Now you can call your sync action and set up a BigCommerce App Extension in the install action.

You will provide a url for the App Extension that you will hook up to a frontend route soon: "/products/${id}/size-chart".

  1. Paste the following in api/models/bigcommerce/store/actions/install.js:
api/models/bigcommerce/store/actions/install.js
JavaScript
1import { applyParams, save, ActionOptions, InstallBigcommerceStoreActionContext } from "gadget-server";
2
3/**
4 * @param { InstallBigcommerceStoreActionContext } context
5 */
6export async function run({ params, record, logger, api, connections }) {
7 applyParams(params, record);
8 await save(record);
9}
10
11/**
12 * @param { InstallBigcommerceStoreActionContext } context
13 */
14export async function onSuccess({ params, record, logger, api, connections }) {
15 // use fetch for GraphQL request (GraphQL not supported by built-in client)
16 const response = await fetch(`https://api.bigcommerce.com/stores/${record.storeHash}/graphql`, {
17 method: "POST",
18 headers: {
19 "Content-Type": "application/json",
20 Accept: "application/json",
21 "X-Auth-Token": record.accessToken,
22 },
23 body: JSON.stringify({
24 query: `mutation AppExtension($input: CreateAppExtensionInput!) {
25 appExtension {
26 createAppExtension(input: $input) {
27 appExtension {
28 id
29 context
30 model
31 url
32 label {
33 defaultValue
34 locales {
35 value
36 localeCode
37 }
38 }
39 }
40 }
41 }
42 }`,
43 variables: {
44 // edit input to match your desired App Extension
45 input: {
46 context: "PANEL",
47 model: "PRODUCTS",
48 url: "/products/${id}/size-chart",
49 label: {
50 defaultValue: "Size chart",
51 locales: [
52 {
53 value: "Size chart",
54 localeCode: "en-US",
55 },
56 ],
57 },
58 },
59 },
60 }),
61 });
62
63 const jsonResponse = await response.json();
64
65 if (jsonResponse.errors) {
66 logger.error({ errors: jsonResponse.errors }, "Error creating app extension");
67 }
68
69 // save the App Extension id to your bigcommerce/store model
70 await api.internal.bigcommerce.store.update(record.id, {
71 appExtensionId: jsonResponse.data.appExtension.createAppExtension.appExtension.id,
72 });
73
74 // sync existing product data from BigCommerce store to Gadget db
75 // use background action so install is not blocked
76 await api.enqueue(api.bigcommerce.syncProducts, { storeHash: record.storeHash });
77}
78
79/** @type { ActionOptions } */
80export const options = {
81 actionType: "create",
82};

This action:

  • uses a GraphQL request to set up an App Extension
  • stores the ID of the App Extension on the bigcommerce/store model
  • kicks off a data sync by calling api.syncProducts()

For more details on working with App Extensions in Gadget, read our docs.

Running the install action 

You have already installed your app on a store. This means that to run this action you need to:

  • uninstall the app from your sandbox store
  • delete the store record in Gadget at api/models/bigcommerce/store/data
  • reinstall the app on your sandbox store

Note: For production apps, you may want to run this same code in both the bigcommerce/store/install and bigcommerce/store/reinstall actions.

Step 6: Access control 

All Gadget apps have authorization built in. A role-based access control system allows us to restrict API and data access to merchants and shoppers.

The bigcommerce-app-users role is automatically assigned to merchants who install your app in BigCommerce. Storefront shoppers will be granted the unauthenticated role. You can read more about roles in our docs.

You need to grant both merchants and shoppers access to the actions that power size chart frontends:

  1. Navigate to the accessControl/permissions page.
  2. Grant the bigcommerce-app-users role access to the bigcommerce/product/ model's read and update actions. This will allow merchants to create and save size charts for products.
  3. Grant the unauthenticated role access to the bigcommerce/product model's read API. This allows shoppers to read the size charts in a storefront.

Read our data security and multi-tenancy docs to learn how to secure your actions when building apps that will be installed on multiple stores.

Step 7: App Extension Size Chart code 

Now you are ready to build your App Extension frontend. Gadget frontends are built with React and all code is located in the web folder. BigDesign has also been installed for you.

An API client has been set up for you in web/api.js. While you build, and add model and actions to your app, Gadget will keep this client up to date. You will use this client, along with React hooks from the @gadgetinc/react package, to call your actions and save changes to size charts.

Start by adding a new route component to your frontend. The next step will be defining this route on the frontend router.

  1. Create a new file size-chart.jsx in web/routes and paste the following code:
web/routes/size-chart.jsx
React
1import { Box, Button, Flex, FlexItem, Form, H2, Input, Message, Panel, StatefulTable, Text } from "@bigcommerce/big-design";
2import { AddIcon, RemoveIcon } from "@bigcommerce/big-design-icons";
3import { useAction, useFindFirst } from "@gadgetinc/react";
4import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
5import { useParams } from "react-router-dom";
6import { api } from "../api";
7
8// sample data used when no size chart is present
9const SAMPLE_COLUMNS = [{ hash: "column1" }, { hash: "column2" }, { hash: "column3" }, { hash: "column4" }];
10const SAMPLE_ITEMS = [
11 {
12 id: "header",
13 column1: "Size",
14 column2: "S",
15 column3: "M",
16 column4: "L",
17 },
18 {
19 id: "row1",
20 column1: "Chest",
21 column2: '34-36"',
22 column3: '37-38"',
23 column4: '39-40"',
24 },
25 {
26 id: "row2",
27 column1: "Waist",
28 column2: '30-32"',
29 column3: '33-34"',
30 column4: '35-36"',
31 },
32];
33
34// buttons for adding or removing columns and rows to table
35const TableButtons = ({ name, onAdd, onRemove, disableRemove }) => (
36 <Flex style={{ width: "250px", padding: "10px" }} flexDirection="row" justifyContent="space-between" alignItems="center">
37 <FlexItem flexShrink={1}>
38 <Text>{name}</Text>
39 </FlexItem>
40 <FlexItem>
41 <Flex flexDirection="row" alignItems="center" style={{ gap: "4px" }}>
42 <FlexItem>
43 <Button iconOnly={<RemoveIcon title="remove" />} onClick={() => onRemove()} disabled={disableRemove} type="button" />
44 </FlexItem>
45 <FlexItem>
46 <Button iconOnly={<AddIcon title="add" />} onClick={() => onAdd()} type="button" />
47 </FlexItem>
48 </Flex>
49 </FlexItem>
50 </Flex>
51);
52
53// memoized cell that can be edited
54const EditableCell = memo(({ initialValue, onSave }) => {
55 const [value, setValue] = useState(initialValue);
56
57 const handleChange = (e) => {
58 setValue(e.target.value);
59 };
60
61 const handleBlur = () => {
62 if (value !== initialValue) {
63 onSave(value);
64 }
65 };
66
67 return <Input type="text" value={value} onChange={handleChange} onBlur={handleBlur} />;
68});
69
70// the actual size chart
71const SizeChart = ({ product }) => {
72 // call Gadget action to save size chart
73 const [{ data: savedChart, fetching: savingChart, error: chartSaveError }, saveSizeChart] = useAction(api.bigcommerce.product.update);
74
75 // state for the chart (in BigDesign StatefulTable input format)
76 const [columns, setColumns] = useState(product.sizeChart?.columns || SAMPLE_COLUMNS);
77
78 const [items, setItems] = useState(product.sizeChart?.items || SAMPLE_ITEMS);
79
80 // state for managing error and success message visibility
81 const [isSaveVisible, setIsSaveVisible] = useState(false);
82 const [isErrorVisible, setIsErrorVisible] = useState(false);
83
84 // manage message visibility
85 useEffect(() => {
86 if (!savingChart && savedChart) {
87 setIsSaveVisible(true);
88 }
89 }, [savingChart, savedChart]);
90
91 useEffect(() => {
92 if (chartSaveError) {
93 setIsErrorVisible(true);
94 }
95 }, [chartSaveError]);
96
97 // update state when a cell is edited
98 const handleCellChange = useCallback((itemId, columnHash, value) => {
99 setItems((prevItems) => prevItems.map((item) => (item.id === itemId ? { ...item, [columnHash]: value } : item)));
100 if (itemId === "header") {
101 setColumns((prevColumns) => prevColumns.map((col) => (col.hash === columnHash ? { ...col, header: value } : col)));
102 }
103 }, []);
104
105 // render editable (and memoized) cells
106 const renderCell = (handleCellChange) => (column) => {
107 return ({ [column.hash]: value, id }) => (
108 <EditableCell
109 key={`${id}-${column.hash}`}
110 initialValue={value || ""}
111 onSave={(newValue) => handleCellChange(id, column.hash, newValue)}
112 />
113 );
114 };
115
116 // memoized cells prevent re-renders of the whole table when state is updated
117 const memoizedRenderCell = useCallback(renderCell(handleCellChange), [handleCellChange]);
118
119 // columns rendered in the table
120 const tableColumns = useMemo(
121 () =>
122 columns.map((column) => ({
123 ...column,
124 render: memoizedRenderCell(column),
125 })),
126 [columns, memoizedRenderCell]
127 );
128
129 // callback for adding a new column
130 // edit column and item state
131 const addColumn = useCallback(() => {
132 const newColumnHash = `column${columns.length + 1}`;
133 setColumns((prevColumns) => [...prevColumns, { hash: newColumnHash }]);
134 setItems((prevItems) =>
135 prevItems.map((item, index) => ({
136 ...item,
137 [newColumnHash]: index === 0 ? newColumnHash : "",
138 }))
139 );
140 }, [columns.length]);
141
142 // callback for removing a new column
143 // edit column and item state
144 const removeColumn = useCallback(() => {
145 if (columns.length > 2) {
146 const updatedColumns = [...columns];
147 const removedColumn = updatedColumns.pop();
148 setColumns(updatedColumns);
149 setItems((prevItems) => prevItems.map(({ [removedColumn.hash]: _, ...rest }) => rest));
150 }
151 }, [columns]);
152
153 // edit item state when a row is added
154 const addRow = useCallback(() => {
155 const newRow = {
156 id: `row${items.length}`,
157 ...Object.fromEntries(columns.map((col) => [col.hash, ""])),
158 };
159 setItems((prevItems) => [...prevItems, newRow]);
160 }, [columns, items.length]);
161
162 // edit item state when a row is removed
163 const removeRow = useCallback(() => {
164 if (items.length > 2) {
165 setItems((prevItems) => prevItems.slice(0, -1));
166 }
167 }, [items.length]);
168
169 // save the chart
170 const onSubmit = useCallback(
171 async (event) => {
172 event.preventDefault();
173
174 setIsSaveVisible(false);
175 await saveSizeChart({
176 id: product.id,
177 sizeChart: { columns, items },
178 });
179 },
180 [items]
181 );
182
183 return (
184 <>
185 <H2>Size chart for {product.name}</H2>
186 {!product.sizeChart && !savedChart && <Text>Sample chart provided. Make edits and save.</Text>}
187 {isSaveVisible && <Message header="Size chart saved" onClose={() => setIsSaveVisible(false)} />}
188 {isErrorVisible && (
189 <Message
190 header="Error on save"
191 type="error"
192 messages={[
193 {
194 text: `Error: ${chartSaveError.message}`,
195 },
196 ]}
197 onClose={() => setIsErrorVisible(false)}
198 />
199 )}
200 <Form onSubmit={onSubmit}>
201 <StatefulTable columns={tableColumns} items={items} keyField="id" headerless />
202 <Box marginTop="medium">
203 <TableButtons name="Columns" onAdd={addColumn} onRemove={removeColumn} disableRemove={columns.length <= 2} />
204
205 <TableButtons name="Rows" onAdd={addRow} onRemove={removeRow} disableRemove={items.length <= 2} />
206 </Box>
207 <Box marginTop="medium">
208 <Button type="submit" disabled={savingChart}>
209 Save chart
210 </Button>
211 </Box>
212 </Form>
213 </>
214 );
215};
216
217export default function () {
218 const { productId } = useParams();
219
220 const [{ data: product, fetching, error }] = useFindFirst(api.bigcommerce.product, {
221 select: { id: true, name: true, sizeChart: true },
222 filter: { bigcommerceId: { equals: parseInt(productId) } },
223 });
224
225 return (
226 <Panel>
227 {error && (
228 <Message
229 header="Error fetching chart"
230 type="error"
231 messages={[
232 {
233 text: `Error: ${chartSaveError.message}`,
234 },
235 ]}
236 onClose={() => setIsErrorVisible(false)}
237 />
238 )}
239 {fetching ? <Text>Loading...</Text> : <SizeChart product={{ bigcommerceId: productId, ...product }} />}
240 </Panel>
241 );
242}

This frontend route:

  • gets the productId from the params
  • loads existing product data, including a size chart if it already exists
  • renders a dynamic table component that can be customized
    • cells are memoized to prevent redraws on Input updates
    • state is managed with useState hooks
  • saves a size chart as json by calling the api.bigcommerce.product.update action
@gadgetinc/react hooks

Gadget provides the @gadgetinc/react library which contains useful hooks and tools for building React frontends. The useFindMany, useAction, and useActionForm hooks are used to fetch data, call actions, and manage form state, respectively.

  1. Now add the route definition to the frontend router in web/components/App.jsx:
web/components/App.jsx
React
1// .. additional imports
2
3import SizeChart from "../routes/size-chart";
4
5// add the new route in the App component
6function App() {
7 const router = createBrowserRouter(
8 createRoutesFromElements(
9 <Route path="/" element={<Layout />}>
10 <Route index element={<Index />} />
11 <Route path="*" element={<Error404 />} />
12 {/** add the size chart route */}
13 <Route path="/products/:productId/size-chart" element={<SizeChart />} />
14 </Route>
15 )
16 );
17
18 return <RouterProvider router={router} />;
19}

Testing your App Extension 

Now you can test your App Extension. Open up the Size chart extension for a product in your store.

Make sure you select one of the 50 products that was synced to your Gadget database. You can see what products were synced at api/models/bigcommerce/product/data.

You can modify the number of rows and columns in the chart, and edit the chart contents/measurements.

Once you are done, click Save chart and the chart config will be saved to Gadget as JSON.

Step 8: Draw size chart in Catalyst product page 

The last step is rendering the custom size charts on the product pages of a Catalyst storefront.

You need to install a copy of your API client into your Catalyst project to read size chart data from your Gadget models.

Your API client package will have the format @gadget-client/<YOUR-APP-SLUG>.

  1. Set up a Catalyst project and start your dev server. This can be an existing storefront or a new project.
  2. cd into your Catalyst project's core folder.
  3. Install your Gadget API client:
terminal
pnpm install @gadget-client/example-app

You may also need to register the Gadget NPM repository.

terminal
npm config set @gadget-client:registry https://registry.gadget.dev/npm

  1. Create a new file gadget.ts in core/, then initialize and export your client:
core/gadget.ts
TypeScript
import { Client } from "@gadget-client/example-app";
export const api = new Client();
  1. Create a size-chart.tsx file in core/app/[locale]/(default)/product/[slug]/_components and paste the following code:
core/app/[locale]/(default)/product/[slug]/_components/size-chart.jsx
JavaScript
1import React from "react";
2import { api } from "../../../../../../gadget";
3
4// Define types for the size chart data
5interface SizeChartItem {
6 id: string;
7 [key: string]: string;
8}
9
10interface SizeChartData {
11 items: SizeChartItem[];
12 columns: { hash: string }[];
13}
14
15// SizeChart component
16export const SizeChart: React.FC<{ productId: number }> = async ({ productId }) => {
17 // fetch size chart for product from Gadget API
18 const response = await api.bigcommerce.product.findFirst({
19 filter: {
20 bigcommerceId: {
21 equals: productId, // select the size chart for this product
22 },
23 store: {
24 storeHash: {
25 equals: process.env.BIGCOMMERCE_STORE_HASH, // for multi-tenant apps, select the size chart for this store
26 },
27 },
28 },
29 select: {
30 sizeChart: true, // only return the size chart field
31 },
32 });
33
34 const { sizeChart } = response.toJSON();
35
36 // no size chart available, return nothing!
37 if (!sizeChart) {
38 return null;
39 }
40
41 /// map json response to SizeChartData type
42 const data: SizeChartData = {
43 items: (sizeChart as any).items as SizeChartItem[],
44 columns: (sizeChart as any).columns as { hash: string }[],
45 };
46
47 return (
48 <>
49 <h2 className="mb-4 text-xl font-bold md:text-2xl">Size chart</h2>
50 <table className="table-auto border-collapse border border-gray-300">
51 <thead>
52 <tr>
53 {data.columns.map((column) => (
54 <th key={column.hash} className="border border-gray-300 p-2">
55 {data.items[0] && data.items[0][column.hash as keyof SizeChartItem]}
56 </th>
57 ))}
58 </tr>
59 </thead>
60 <tbody>
61 {data.items.slice(1).map((item) => (
62 <tr key={item.id}>
63 {Object.entries(item)
64 .filter(([key, _value]) => key !== "id")
65 .map(([key, value], index) => (
66 <td key={`${key}_${index}`} className="border border-gray-300 p-2">
67 {value}
68 </td>
69 ))}
70 </tr>
71 ))}
72 </tbody>
73 </table>
74 </>
75 );
76};

This component:

  • uses your api client to read product size chart data
  • renders a size chart, if data is returned from Gadget
  1. Import the SizeChart component into your product page at core/app/[locale]/(default)/product/[slug]/page.tsx and render the chart:
core/app/[locale]/(default)/product/[slug]/page.jsx
JavaScript
1// .. other imports
2import { SizeChart } from "./_components/size-chart";
3
4// .. interface, type, and function definitions
5
6export default async function Product({ params: { locale, slug }, searchParams }: Props) {
7 return (
8 <>
9 {/* other components such as Breadcrumbs */}
10
11 <div className="mb-12 mt-4 lg:grid lg:grid-cols-2 lg:gap-8">
12 {/* other components such as Gallery and Details */}
13 <div className="lg:col-span-2">
14 <Description product={product} />
15
16 {/* other components such as Reviews */}
17
18 <Suspense fallback={t("loading")}>
19 <SizeChart productId={product.entityId} />
20 </Suspense>
21 </div>
22 </div>
23
24 {/* rest of the page */}
25 </>
26 );
27}

For more information on building with Catalyst storefronts, read our docs.

Test it out 

You have just built a full stack size chart app, congrats!

You can preview your custom size charts on the storefront by navigating to a product page for which you have built and saved a size chart.

Next steps 

  • Join Gadget's developer community on Discord

You can extend and customize this app. Some possibilities include:

  • importing existing size charts as a CSV into your Gadget database using an action
  • displaying the size chart in a model on the storefront