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-listinmanifest.json(nested options) destinationfields (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.locationvalues with real selections from App Studio - Replace placeholder image URLs with your own assets
Updated 25 days ago