Example - Collection Bar (useCollection)
A configurable collection bar demonstrating useCollection, loading, and empty states.
This example demonstrates a common real-world pattern:
- Fetching collections using the
useCollectionhook - Handling loading states (
Skeleton) - Handling empty/error states (
EmptyMessage) - Using
manifest.jsonto configure sticky positioning + typography + padding
Code
import * as React from 'react';
import {
ScrollArea,
Text,
useCollection,
getTextStyle,
getBackgroundAndPaddingStyle,
EmptyMessage,
useRouter,
usePathname,
Skeleton,
getIdFromGid,
useScrollDirection,
useStyleParent,
} from '@tapcart/mobile-components';
function CarouselLoading({ borderRadius = 6 }) {
const Item = () => (
<div className="w-16 h-4 flex items-center justify-center p-0">
<Skeleton className="w-full h-full" style={{ borderRadius: `${borderRadius}px` }} />
</div>
);
return (
<div className="flex space-x-2">
{Array(8)
.fill(0)
.map((_, index) => (
<Item key={index} />
))}
</div>
);
}
export default function CollectionBarExample({
blockConfig,
tapcartData,
translations,
pageState,
useTapcart,
useSearchParams,
__tapcartDashboard,
}) {
const Tapcart = useTapcart();
const searchParams = useSearchParams();
const lang = searchParams.get('lang') || pageState.locale;
const { direction, scrollY } =
React.version === '17.0.2' ? { direction: null, scrollY: 0 } : useScrollDirection();
const dashboardDirection = __tapcartDashboard
? { direction: null, scrollY: 0 }
: { direction, scrollY };
const ref = useStyleParent(
blockConfig?.position?.sticky
? {
position: 'sticky',
top: '0px',
zIndex: __tapcartDashboard ? 'initial' : '50',
}
: {}
);
const { collections: configCollections, collectionTitle, backgroundAndPadding } = blockConfig;
const getCollectionHookParams = React.useCallback(
(config) => {
const params = {
apiUrl: pageState.baseAPIURL,
appId: tapcartData.appId,
language: lang,
};
if (config?.allCollections) {
return { ...params, getCollections: true, limit: 15 };
}
if (!config?.collections?.length) {
return { ...params, getCollections: true, limit: 3 };
}
const collectionIdList = config.collections.map((c) => c.id);
return { ...params, collectionIdList };
},
[pageState.baseAPIURL, lang, tapcartData.appId]
);
const params = React.useMemo(
() => getCollectionHookParams(configCollections),
[configCollections, getCollectionHookParams]
);
const { collections, loading } = useCollection(params);
const {
paddingLeft: containerPaddingLeft,
paddingRight: containerPaddingRight,
...containerStyle
} = React.useMemo(
() => (backgroundAndPadding?.enabled ? getBackgroundAndPaddingStyle(backgroundAndPadding) : {}),
[backgroundAndPadding]
);
const collectionNameStyle = React.useMemo(
() => (collectionTitle?.collectionName?.enabled ? getTextStyle(collectionTitle.collectionName) : {}),
[collectionTitle?.collectionName]
);
const CollectionBarItem = ({ collection }) => {
const handleClick = () => {
Tapcart.action?.('trigger/haptic');
Tapcart.actions?.openCollection({
collectionId: getIdFromGid(collection.id),
});
};
return (
<div
key={collection.id}
className="cursor-pointer relative"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleClick();
}}
onClick={handleClick}
>
{collectionTitle?.collectionName?.enabled && (
<div className="flex items-center">
<Text className="truncate w-full" style={collectionNameStyle}>
{collection.title}
</Text>
</div>
)}
</div>
);
};
if (!collections?.length && !loading) {
return (
<div className="flex h-48">
<EmptyMessage
iconName="mood-sad"
title={translations['collection-empty-title']}
buttonLabel={translations['collection-empty-button']}
className="mx-4"
openScreen={Tapcart.actions?.openScreen}
useRouter={useRouter}
usePathname={usePathname}
useSearchParams={useSearchParams}
/>
</div>
);
}
if (blockConfig?.position?.sticky) {
containerStyle.outline = `8px solid ${containerStyle.backgroundColor}`;
}
if (blockConfig?.position?.autoHideOnScroll) {
containerStyle.top = dashboardDirection.direction === 'down' ? '-30px' : '0';
containerStyle.opacity = dashboardDirection.direction === 'down' ? 0 : 1;
containerStyle.transition =
dashboardDirection.scrollY === 0 && blockConfig?.position?.autoHideOnScroll
? 'none'
: 'top 0.3s ease-in-out, opacity 0.3s ease-in-out';
containerStyle.position = 'relative';
}
return (
<div ref={ref}>
<div className="flex flex-col" style={containerStyle}>
{collectionTitle?.enabled && (
<ScrollArea
wrapperClass="space-x-4"
wrapperStyle={{
paddingLeft: containerPaddingLeft,
paddingRight: containerPaddingRight,
}}
scrollbar={false}
>
{loading && <CarouselLoading />}
{collections?.map((item) => (
<CollectionBarItem key={item.id} collection={item} />
))}
</ScrollArea>
)}
</div>
</div>
);
}Manifest
[
{
"id": "collections",
"label": "Content",
"type": "collection-list",
"enableAllCollectionsOption": true,
"defaultValue": {
"allCollections": true,
"collections": []
}
},
{
"id": "position",
"label": "Position",
"type": "section",
"icon": "hand-click",
"defaultValue": true,
"manifestOptions": [
{
"id": "sticky",
"label": "Sticky",
"type": "toggle",
"defaultValue": true
},
{
"id": "autoHideOnScroll",
"label": "Hide/Show on scroll",
"type": "toggle",
"defaultValue": true
}
]
},
{
"id": "collectionTitle",
"label": "Collection Title",
"type": "page",
"icon": "text-size",
"defaultValue": true,
"manifestOptions": [
{
"id": "collectionName",
"label": "Collection Name",
"type": "section",
"icon": "text-size",
"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": 13,
"min": 1,
"max": 50,
"unit": "px",
"step": 1
},
{
"id": "color",
"label": "Color",
"type": "color-select",
"defaultValue": {
"type": "brand-kit",
"value": "textColors-primaryColor"
}
},
{
"id": "uppercase",
"label": "Uppercase",
"type": "toggle",
"defaultValue": false
},
{
"id": "textAlignment",
"label": "Alignment",
"type": "text_alignment",
"defaultValue": "center"
}
]
}
]
},
{
"id": "backgroundAndPadding",
"label": "Background & Padding",
"type": "page",
"icon": "background",
"defaultValue": true,
"manifestOptions": [
{
"type": "header",
"label": "Background"
},
{
"id": "backgroundColor",
"label": "Color",
"type": "color-select",
"defaultValue": {
"type": "brand-kit",
"value": "coreColors-pageColor"
}
},
{
"id": "cornerRadius",
"label": "Corners",
"type": "range",
"defaultValue": 0,
"min": 0,
"max": 50,
"unit": "px",
"step": 1
},
{
"type": "divider"
},
{
"type": "header",
"label": "Border"
},
{
"id": "borderColor",
"label": "Color",
"type": "color-select",
"defaultValue": {
"type": "brand-kit",
"value": null
}
},
{
"id": "borderSides",
"label": "Sides",
"type": "border-sides",
"defaultValue": []
},
{
"type": "header",
"label": "Padding"
},
{
"id": "padding",
"type": "padding",
"label": "",
"defaultValue": {
"top": 8,
"bottom": 8,
"left": 16,
"right": 16
}
}
]
}
]Mock Data (optional)
This example does not use useVariables, so mockData.json is not required.
Important:
useCollectionfetches data from Tapcart APIs.mockData.jsondoes not mockuseCollectionresults.
If you still want to include a mockData.json file for experimentation with useVariables, you can add one next to code.jsx:
{
"device": {
"locale": "en-US"
}
}Updated 25 days ago