Collections with products carousel

Collections with products carousel

shopifycollectionslist
import React from 'react';
import { Actionable, Box, Flexbox, Image, Text, Icon } from '@evlop/native-components';
import { useProducts } from '@evlop/shopify';
import { memoize } from 'lodash';

export default function App(props: NativeAppBlockProps) {
  const collection = props.data?.collection ?? props.data;
  const products = useProducts({ params: JSON.stringify({collectionId: collection?.id}), pageSize: 5 });

  if (!collection) return null;

  const layouts = calculateLayouts(products.length);

  return (
    <Actionable action={collection.actions?.openDetailsPage}>
      <Flexbox bg="gray-50" p="2xs" flexDirection="column" gap={10}  borderRadius={10}>
        <Flexbox alignItems="center" justifyContent="space-between">
          <Text textAlign="center" fontSize="3xl" color="gray-900" fontWeight="Semibold">
            {collection.title}
          </Text>
          <Icon size={16} icon="feather:chevron-right" color="gray-500" />
        </Flexbox>

        {/* Collage container. We use a fixed aspect box so the absolute % positions work predictably. */}
        <Box position="relative" backgroundColor="gray-0/700" width="100%" style={{aspectRatio: 1}}>
          {products.map((p: any, i: number) => {
            const layout = layouts[i];
            if (!layout) return null;

            return (
              <Actionable key={p.id || p.handle || i} action={p.actions?.openDetailsPage} style={layout}>
                <Image
                  src={p.thumbnail}
                  resizeMode="cover"
                  width="100%"
                  height="100%"
                />
              </Actionable>
            );
          })}
        </Box>
      </Flexbox>
    </Actionable>
  );
}

// Calculate absolute positions/sizes using percentages so the collage adapts to any container size.
// Returns an array of { left, top, width, height } strings (percentages) for each product index.
const calculateLayouts = memoize((count: number) => {
  const layouts = [];

  switch (count) {
    case 1:
      // single full-bleed image
      layouts.push({ left: 0, top: 0, width: '100%', height: '100%' });
      break;
    case 2:
      // two equal columns
      layouts.push({ left: 0, top: 0, width: '50%', height: '100%' });
      layouts.push({ left: '50%', top: 0, width: '50%', height: '100%' });
      break;
    case 3:
      // left column large, right column split in two
      layouts.push({ left: 0, top: 0, width: '50%', height: '100%' });
      layouts.push({ left: '50%', top: 0, width: '50%', height: '50%' });
      layouts.push({ left: '50%', top: '50%', width: '50%', height: '50%' });
      break;
    case 4:
      // 2x2 grid
      layouts.push({ left: 0, top: 0, width: '50%', height: '50%' });
      layouts.push({ left: '50%', top: 0, width: '50%', height: '50%' });
      layouts.push({ left: 0, top: '50%', width: '50%', height: '50%' });
      layouts.push({ left: '50%', top: '50%', width: '50%', height: '50%' });
      break;
    default:
      // 5 or more: left column two stacked (50% each), right column three stacked (33.33% each)
      layouts.push({ left: 0, top: 0, width: '50%', height: '50%' });
      layouts.push({ left: 0, top: '50%', width: '50%', height: '50%' });
      layouts.push({ left: '50%', top: 0, width: '50%', height: '33.3333%' });
      layouts.push({ left: '50%', top: '33.3333%', width: '50%', height: '33.3333%' });
      layouts.push({ left: '50%', top: '66.6666%', width: '50%', height: '33.3334%' });
      break;
  }

  return layouts.map(l=>({...l, position: 'absolute', overflow: 'hidden', borderLeftWidth: l.left === 0? 0 : 1,  borderTopWidth: l.top == 0? 0 : 1, borderColor: 'transparent'}));
})
Loading...