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 useCollection hook
  • Handling loading states (Skeleton)
  • Handling empty/error states (EmptyMessage)
  • Using manifest.json to 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:

  • useCollection fetches data from Tapcart APIs.
  • mockData.json does not mock useCollection results.

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"
  }
}

Back to Examples