Example - Collections Menu (flex-list + destination + drawer)

A configurable collections menu using flex-list, destinations, and a drawer UI.

This example is designed to show partners how to build a highly-configurable block:

  • A flex-list in manifest.json (nested options)
  • destination fields (collection/product/screen)
  • A drawer UI to display subcollections

Code

import * as React from 'react';
import {
  Image,
  Text,
  getTextStyle,
  getBackgroundAndPaddingStyle,
  getDestinationHandler,
  Drawer,
  DrawerContent,
  DrawerContentBase,
  DrawerHeader,
  getColor,
} from '@tapcart/mobile-components';

const mockCollections = {
  subcollections: [
    { _title: 'Subcollection 1', destination: { type: 'none' } },
    { _title: 'Subcollection 2', destination: { type: 'none' } },
    { _title: 'Subcollection 3', destination: { type: 'none' } },
  ],
};

const throttle = (func, limit) => {
  let inThrottle;
  return function () {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
};

const CollectionGridItem = ({ index, collection, settings, handleOnClick, totalItems }) => {
  const { imageSrc, _title } = collection;
  const { titleSettings, imageSettings, backgroundAndPadding, columns, fillGridSpaces, spaceBetween } = settings;

  const remainingItems = totalItems % columns;
  const shouldSpanRemaining = index === 0 && fillGridSpaces && remainingItems > 0;
  const spanSize = columns - (remainingItems - 1);

  const titleBackgroundAndPaddingStyle = {
    ...getTextStyle(titleSettings),
    ...getBackgroundAndPaddingStyle(titleSettings),
    margin: `${titleSettings.margin.top}px ${titleSettings.margin.right}px ${titleSettings.margin.bottom}px ${titleSettings.margin.left}px`,
  };

  const halfSpaceBetween = spaceBetween / 2;

  const cardStyle = {
    display: 'block',
    position: 'relative',
    overflow: 'hidden',
    cursor: 'pointer',
    borderRadius: `${backgroundAndPadding.cornerRadius}px`,
    backgroundColor: getColor(backgroundAndPadding.itemBackgroundColor),
    boxShadow: backgroundAndPadding.showDropshadow ? '0 3px 5px 0 rgba(0, 0, 0, 0.1)' : 'none',
    margin: `${halfSpaceBetween}px`,
    ...(shouldSpanRemaining ? { gridColumn: `span ${spanSize}` } : {}),
    ...(imageSettings.enabled ? { aspectRatio: imageSettings.imageRatio } : {}),
  };

  const titleContainerStyle = imageSettings.enabled
    ? { display: 'flex', justifyContent: titleSettings.textAlignment, position: 'absolute', bottom: 0, left: 0, right: 0 }
    : { display: 'flex', justifyContent: titleSettings.textAlignment };

  return (
    <div
      onClick={handleOnClick}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') handleOnClick();
      }}
      role="button"
      tabIndex={0}
      style={cardStyle}
    >
      {imageSettings.enabled && imageSrc && (
        <Image data={imageSrc} style={{ objectPosition: imageSettings.imagePosition }} alt={_title || ''} />
      )}
      {titleSettings.enabled && (
        <div style={titleContainerStyle}>
          <Text style={titleBackgroundAndPaddingStyle}>{_title}</Text>
        </div>
      )}
    </div>
  );
};

const SubcollectionDrawerContent = ({ collection, settings, getDestinationClickHandler }) => {
  if (!collection) return null;

  return (
    <div
      style={{
        ...getBackgroundAndPaddingStyle(settings.backgroundAndPadding),
        borderRadius: '0px',
        maxHeight: '100%',
        overflow: 'scroll',
      }}
    >
      <div className={`grid grid-cols-${settings.columns}`} style={{ width: '100%', overflowY: 'scroll' }}>
        {collection?.subcollections?.map((subcollection, i) => {
          const onClick = getDestinationClickHandler(subcollection.destination);
          return (
            <CollectionGridItem
              key={`Subcollection-${i}`}
              index={i}
              collection={subcollection}
              settings={settings}
              handleOnClick={onClick}
              totalItems={collection.subcollections.length}
            />
          );
        })}
      </div>
    </div>
  );
};

const SubCollectionDrawer = ({ collection, isOpen, onClose, settings, getDestinationClickHandler }) => {
  return (
    <Drawer open={isOpen} onClose={onClose}>
      <DrawerContent>
        <DrawerContentBase style={{ backgroundColor: getColor(settings.backgroundColor) || '#eee' }}>
          <div style={{ height: 0, overflow: 'hidden' }}>
            <DrawerHeader title={collection?._title || 'Subcollection'} />
          </div>
          <SubcollectionDrawerContent
            collection={collection}
            settings={settings}
            getDestinationClickHandler={getDestinationClickHandler}
          />
        </DrawerContentBase>
      </DrawerContent>
    </Drawer>
  );
};

export default function CollectionsMenuExample({ blockConfig, useTapcart, __tapcartDashboard }) {
  const Tapcart = useTapcart();
  const [isOpen, setIsOpen] = React.useState(false);
  const [canOpen, setCanOpen] = React.useState(true);
  const [selectedCollection, setSelectedCollection] = React.useState(null);

  const { collections, collectionListSettings, subcollectionSettings } = blockConfig;

  const showDashboardSubcollections = __tapcartDashboard && subcollectionSettings?.showSubcollections;
  const dashboardDefaultSelectedCollection =
    __tapcartDashboard && collections?.[0]?.subcollections?.length ? collections[0] : mockCollections;

  const destinationActions = {
    openScreen: Tapcart.actions?.openScreen,
    openProduct: Tapcart.actions?.openProduct,
    openCollection: Tapcart.actions?.openCollection,
  };

  const getDestinationClickHandler = (destination) => {
    const openDestination = getDestinationHandler(destination.type);
    return () => {
      openDestination(destination.location, destinationActions);
      Tapcart.action?.('trigger/haptic');
    };
  };

  const handleCollectionClick = (collection) => {
    if (canOpen && !isOpen) {
      setSelectedCollection(collection);
      setIsOpen(true);
    }
  };

  const handleDrawerClose = throttle(() => {
    setIsOpen(false);
    setCanOpen(false);
    setTimeout(() => setCanOpen(true), 300);
  }, 400);

  if (showDashboardSubcollections) {
    return (
      <SubcollectionDrawerContent
        collection={dashboardDefaultSelectedCollection}
        settings={subcollectionSettings}
        getDestinationClickHandler={getDestinationClickHandler}
      />
    );
  }

  return (
    <div style={{ ...getBackgroundAndPaddingStyle(collectionListSettings.backgroundAndPadding), borderRadius: '0px', overflow: 'hidden' }}>
      <div className={`grid`}>
        <div className={`grid ${collectionListSettings.columns === '2' ? 'grid-cols-2' : 'grid-cols-1'}`}>
          {collections.map((collection, i) => {
            const onClick =
              collection.type === 'link' && collection.destination.type !== 'none'
                ? getDestinationClickHandler(collection.destination)
                : () => handleCollectionClick(collection);

            return (
              <CollectionGridItem
                key={`CollectionsMenu-${i}`}
                index={i}
                collection={collection}
                settings={collectionListSettings}
                handleOnClick={onClick}
                totalItems={collections.length}
              />
            );
          })}
          <SubCollectionDrawer
            collection={selectedCollection}
            isOpen={isOpen}
            onClose={handleDrawerClose}
            settings={subcollectionSettings}
            getDestinationClickHandler={getDestinationClickHandler}
          />
        </div>
      </div>
    </div>
  );
}

Manifest

[
  {
    "id": "collections",
    "label": "Menu Content",
    "type": "flex-list",
    "defaultValue": [
      {
        "_id": "item1",
        "_title": "Collection 1",
        "type": "menu",
        "imageSrc": {
          "url": "https://storage.googleapis.com/cdn.tapcart.com/placeholders/placeholder-1.jpg",
          "naturalAspectRatio": "1:1"
        },
        "destination": {
          "type": "none",
          "location": null
        },
        "subcollections": []
      },
      {
        "_id": "item2",
        "_title": "Collection 2",
        "type": "menu",
        "imageSrc": {
          "url": "https://storage.googleapis.com/cdn.tapcart.com/placeholders/placeholder-2.jpg",
          "naturalAspectRatio": "1:1"
        },
        "destination": {
          "type": "none",
          "location": null
        },
        "subcollections": []
      },
      {
        "_id": "item3",
        "_title": "Collection 3",
        "type": "menu",
        "imageSrc": {
          "url": "https://storage.googleapis.com/cdn.tapcart.com/placeholders/placeholder-3.jpg",
          "naturalAspectRatio": "1:1"
        },
        "destination": {
          "type": "none",
          "location": null
        },
        "subcollections": []
      },
      {
        "_id": "item4",
        "_title": "Collection 4",
        "type": "menu",
        "imageSrc": {
          "url": "https://storage.googleapis.com/cdn.tapcart.com/placeholders/placeholder-4.jpg",
          "naturalAspectRatio": "1:1"
        },
        "destination": {
          "type": "none",
          "location": null
        },
        "subcollections": []
      }
    ],
    "manifestOptions": [
      {
        "id": "type",
        "label": "Type",
        "type": "select",
        "defaultValue": "menu",
        "options": [
          {
            "label": "Menu",
            "value": "menu"
          },
          {
            "label": "Link",
            "value": "link"
          }
        ]
      },
      {
        "id": "imageSrc",
        "label": "Image",
        "type": "asset-upload",
        "description": "Drag or upload an image",
        "supportedFileTypes": [
          "image"
        ],
        "defaultValue": {
          "url": "https://storage.googleapis.com/cdn.tapcart.com/placeholders/placeholder-1.jpg",
          "naturalAspectRatio": "1:1"
        }
      },
      {
        "id": "subcollections",
        "label": "Subcollections",
        "type": "flex-list",
        "info": "Only used if type is Menu",
        "defaultValue": [
          {
            "_id": "item1",
            "_title": "Sub Collection 1",
            "type": "collection",
            "enabled": true,
            "destination": {
              "type": "none",
              "location": null
            }
          }
        ],
        "manifestOptions": [
          {
            "id": "imageSrc",
            "label": "Image",
            "type": "asset-upload",
            "description": "Drag or upload an image",
            "supportedFileTypes": [
              "image"
            ],
            "defaultValue": null
          },
          {
            "id": "destination",
            "label": "Link",
            "type": "destination",
            "defaultValue": {
              "type": "none",
              "location": null
            }
          }
        ]
      },
      {
        "id": "destination",
        "label": "Link",
        "type": "destination",
        "defaultValue": {
          "type": "none",
          "location": null
        }
      }
    ]
  },
  {
    "id": "collectionListSettings",
    "label": "Collection List Settings",
    "type": "page",
    "defaultValue": true,
    "manifestOptions": [
      {
        "id": "columns",
        "label": "Grid Columns",
        "type": "select",
        "defaultValue": "2",
        "options": [
          {
            "label": "1",
            "value": "1"
          },
          {
            "label": "2",
            "value": "2"
          },
          {
            "label": "3",
            "value": "3"
          },
          {
            "label": "4",
            "value": "4"
          }
        ]
      },
      {
        "id": "fillGridSpaces",
        "label": "Fill Extra Grid Spaces",
        "type": "toggle",
        "defaultValue": true
      },
      {
        "id": "spaceBetween",
        "label": "Space Between Grid Items",
        "type": "range",
        "defaultValue": 12,
        "min": 0,
        "max": 50,
        "unit": "px",
        "step": 1
      },
      {
        "id": "backgroundAndPadding",
        "label": "Background & Padding",
        "type": "section",
        "defaultValue": true,
        "manifestOptions": [
          {
            "type": "header",
            "label": "Background"
          },
          {
            "id": "backgroundColor",
            "label": "Background Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "coreColors-pageColor"
            }
          },
          {
            "id": "itemBackgroundColor",
            "label": "Item Background Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "coreColors-pageColor"
            }
          },
          {
            "id": "cornerRadius",
            "label": "Item Corners",
            "type": "range",
            "defaultValue": 12,
            "min": 0,
            "max": 9999,
            "unit": "px",
            "step": 1
          },
          {
            "id": "showDropshadow",
            "label": "Show Drop Shadow",
            "type": "toggle",
            "defaultValue": false
          },
          {
            "type": "header",
            "label": "Padding"
          },
          {
            "id": "padding",
            "type": "padding",
            "label": "",
            "defaultValue": {
              "top": 8,
              "bottom": 8,
              "left": 8,
              "right": 8
            }
          }
        ]
      },
      {
        "id": "imageSettings",
        "label": "Image Settings",
        "type": "section",
        "defaultValue": true,
        "manifestOptions": [
          {
            "type": "header",
            "label": "Size and Scale"
          },
          {
            "id": "imageRatio",
            "label": "Image Ratio",
            "type": "select",
            "defaultValue": "1/1",
            "options": [
              {
                "label": "1:1",
                "value": "1/1"
              },
              {
                "label": "2:3",
                "value": "2/3"
              },
              {
                "label": "4:5",
                "value": "4/5"
              },
              {
                "label": "2:1",
                "value": "2/1"
              },
              {
                "label": "4:1",
                "value": "4/1"
              },
              {
                "label": "5:1",
                "value": "5/1"
              }
            ]
          },
          {
            "id": "imageScale",
            "label": "Image Scale",
            "type": "select",
            "defaultValue": "fill",
            "options": [
              {
                "label": "Fit",
                "value": "fit"
              },
              {
                "label": "Fill",
                "value": "fill"
              }
            ]
          },
          {
            "id": "imagePosition",
            "label": "Image Position",
            "type": "select",
            "defaultValue": "center",
            "options": [
              {
                "label": "Left",
                "value": "left"
              },
              {
                "label": "Center",
                "value": "center"
              },
              {
                "label": "Right",
                "value": "right"
              }
            ]
          }
        ]
      },
      {
        "id": "titleSettings",
        "label": "Title Settings",
        "type": "section",
        "defaultValue": true,
        "manifestOptions": [
          {
            "type": "header",
            "label": "Typography"
          },
          {
            "id": "font",
            "label": "Font",
            "type": "font-select",
            "defaultValue": {
              "family": "unset",
              "weight": "unset"
            }
          },
          {
            "id": "size",
            "label": "Size",
            "type": "range",
            "defaultValue": 16,
            "min": 8,
            "max": 36,
            "unit": "px",
            "step": 1
          },
          {
            "id": "color",
            "label": "Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "buttonColors-primaryText"
            }
          },
          {
            "id": "uppercase",
            "label": "Uppercase",
            "type": "toggle",
            "defaultValue": false
          },
          {
            "type": "divider"
          },
          {
            "type": "header",
            "label": "Background"
          },
          {
            "id": "backgroundColor",
            "label": "Background Color",
            "type": "color-select",
            "defaultValue": {
              "type": null,
              "value": null
            }
          },
          {
            "id": "borderRadius",
            "label": "Corners",
            "type": "range",
            "defaultValue": 4,
            "min": 0,
            "max": 50,
            "unit": "px"
          },
          {
            "type": "divider"
          },
          {
            "type": "header",
            "label": "Border"
          },
          {
            "id": "borderColor",
            "label": "Border Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "buttonColors-primaryOutlineColor"
            }
          },
          {
            "id": "borderSides",
            "label": "Sides",
            "type": "border-sides",
            "defaultValue": [
              "none"
            ]
          },
          {
            "type": "divider"
          },
          {
            "type": "header",
            "label": "Alignment and Padding"
          },
          {
            "id": "textAlignment",
            "label": "Alignment",
            "type": "text_alignment",
            "defaultValue": "center"
          },
          {
            "id": "padding",
            "label": "Padding",
            "type": "padding",
            "defaultValue": {
              "top": 8,
              "bottom": 8,
              "left": 16,
              "right": 16
            }
          },
          {
            "type": "header",
            "label": "Margin"
          },
          {
            "id": "margin",
            "label": "Margin",
            "type": "padding",
            "defaultValue": {
              "top": 0,
              "bottom": 8,
              "left": 0,
              "right": 0
            }
          }
        ]
      }
    ]
  },
  {
    "id": "subcollectionSettings",
    "label": "Subcollection Settings",
    "type": "page",
    "defaultValue": true,
    "manifestOptions": [
      {
        "id": "showSubcollections",
        "label": "Show Subcollections Preview",
        "type": "toggle",
        "defaultValue": false
      },
      {
        "id": "columns",
        "label": "Grid Columns",
        "type": "select",
        "defaultValue": "1",
        "options": [
          {
            "label": "1",
            "value": "1"
          },
          {
            "label": "2",
            "value": "2"
          },
          {
            "label": "3",
            "value": "3"
          },
          {
            "label": "4",
            "value": "4"
          }
        ]
      },
      {
        "id": "fillGridSpaces",
        "label": "Fill Extra Grid Spaces",
        "type": "toggle",
        "defaultValue": true
      },
      {
        "id": "spaceBetween",
        "label": "Space Between Grid Items",
        "type": "range",
        "defaultValue": 8,
        "min": 0,
        "max": 50,
        "unit": "px",
        "step": 1
      },
      {
        "id": "backgroundAndPadding",
        "label": "Background & Padding",
        "type": "section",
        "defaultValue": true,
        "manifestOptions": [
          {
            "type": "header",
            "label": "Background"
          },
          {
            "id": "backgroundColor",
            "label": "Background Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "coreColors-pageColor"
            }
          },
          {
            "id": "itemBackgroundColor",
            "label": "Item Background Color",
            "type": "color-select",
            "defaultValue": {
              "type": "custom",
              "value": "white"
            }
          },
          {
            "id": "cornerRadius",
            "label": "Item Corners",
            "type": "range",
            "defaultValue": 12,
            "min": 0,
            "max": 9999,
            "unit": "px",
            "step": 1
          },
          {
            "id": "showDropshadow",
            "label": "Show Drop Shadow",
            "type": "toggle",
            "defaultValue": false
          },
          {
            "type": "header",
            "label": "Padding"
          },
          {
            "id": "padding",
            "type": "padding",
            "label": "",
            "defaultValue": {
              "top": 8,
              "bottom": 8,
              "left": 8,
              "right": 8
            }
          }
        ]
      },
      {
        "id": "imageSettings",
        "label": "Image Settings",
        "type": "section",
        "defaultValue": false,
        "manifestOptions": [
          {
            "type": "header",
            "label": "Size and Scale"
          },
          {
            "id": "imageRatio",
            "label": "Image Ratio",
            "type": "select",
            "defaultValue": "5/1",
            "options": [
              {
                "label": "1:1",
                "value": "1/1"
              },
              {
                "label": "2:3",
                "value": "2/3"
              },
              {
                "label": "4:5",
                "value": "4/5"
              },
              {
                "label": "2:1",
                "value": "2/1"
              },
              {
                "label": "4:1",
                "value": "4/1"
              },
              {
                "label": "5:1",
                "value": "5/1"
              }
            ]
          },
          {
            "id": "imageScale",
            "label": "Image Scale",
            "type": "select",
            "defaultValue": "fill",
            "options": [
              {
                "label": "Fit",
                "value": "fit"
              },
              {
                "label": "Fill",
                "value": "fill"
              }
            ]
          },
          {
            "id": "imagePosition",
            "label": "Image Position",
            "type": "select",
            "defaultValue": "center",
            "options": [
              {
                "label": "Left",
                "value": "left"
              },
              {
                "label": "Center",
                "value": "center"
              },
              {
                "label": "Right",
                "value": "right"
              }
            ]
          }
        ]
      },
      {
        "id": "titleSettings",
        "label": "Title Settings",
        "type": "section",
        "defaultValue": true,
        "manifestOptions": [
          {
            "type": "header",
            "label": "Typography"
          },
          {
            "id": "font",
            "label": "Font",
            "type": "font-select",
            "defaultValue": {
              "family": "unset",
              "weight": "unset"
            }
          },
          {
            "id": "size",
            "label": "Size",
            "type": "range",
            "defaultValue": 16,
            "min": 8,
            "max": 36,
            "unit": "px",
            "step": 1
          },
          {
            "id": "color",
            "label": "Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "buttonColors-primaryText"
            }
          },
          {
            "id": "uppercase",
            "label": "Uppercase",
            "type": "toggle",
            "defaultValue": false
          },
          {
            "type": "divider"
          },
          {
            "type": "header",
            "label": "Background"
          },
          {
            "id": "backgroundColor",
            "label": "Background Color",
            "type": "color-select",
            "defaultValue": {
              "type": null,
              "value": null
            }
          },
          {
            "id": "borderRadius",
            "label": "Corners",
            "type": "range",
            "defaultValue": 4,
            "min": 0,
            "max": 50,
            "unit": "px"
          },
          {
            "type": "divider"
          },
          {
            "type": "header",
            "label": "Border"
          },
          {
            "id": "borderColor",
            "label": "Border Color",
            "type": "color-select",
            "defaultValue": {
              "type": "brand-kit",
              "value": "buttonColors-primaryOutlineColor"
            }
          },
          {
            "id": "borderSides",
            "label": "Sides",
            "type": "border-sides",
            "defaultValue": [
              "none"
            ]
          },
          {
            "type": "divider"
          },
          {
            "type": "header",
            "label": "Alignment and Padding"
          },
          {
            "id": "textAlignment",
            "label": "Alignment",
            "type": "text_alignment",
            "defaultValue": "center"
          },
          {
            "id": "padding",
            "label": "Padding",
            "type": "padding",
            "defaultValue": {
              "top": 8,
              "bottom": 8,
              "left": 16,
              "right": 16
            }
          },
          {
            "type": "header",
            "label": "Margin"
          },
          {
            "id": "margin",
            "label": "Margin",
            "type": "padding",
            "defaultValue": {
              "top": 0,
              "bottom": 0,
              "left": 0,
              "right": 0
            }
          }
        ]
      }
    ]
  }
]

Mock Data (optional)

This example does not use useVariables, so mockData.json is not required.

If you want to experiment with useVariables during local development, you can add a mockData.json file next to code.jsx and read it in your block via useVariables().

{
  "customer": {
    "id": "<UUID>",
    "firstName": "Taylor",
    "lastName": "Example"
  },
  "device": {
    "locale": "en-US"
  }
}

manifestConfig.json (example defaults)

manifestConfig.json contains the actual values that will be passed into your block as blockConfig.

This example is intentionally sanitized:

  • Image URLs are placeholders
  • Destinations have location: null
{
  "collections": [
    {
      "_id": "item1",
      "_title": "Menu Item 1",
      "type": "menu",
      "imageSrc": {
        "url": "https://storage.googleapis.com/cdn.tapcart.com/placeholders/placeholder-1.jpg",
        "naturalAspectRatio": "1:1"
      },
      "destination": {
        "type": "none",
        "location": null
      },
      "subcollections": [
        {
          "_id": "subitem1",
          "_title": "Subcollection 1",
          "imageSrc": null,
          "destination": {
            "type": "none",
            "location": null
          }
        }
      ]
    }
  ],
  "collectionListSettings": {
    "enabled": true,
    "columns": "2",
    "spaceBetween": 8,
    "fillGridSpaces": false
  },
  "subcollectionSettings": {
    "enabled": true,
    "showSubcollections": false,
    "columns": "1",
    "spaceBetween": 8,
    "fillGridSpaces": true
  }
}

Notes

This doc uses placeholder values so you can copy/paste safely:

  • Replace any destination.location values with real selections from App Studio
  • Replace placeholder image URLs with your own assets

Back to Examples