Skip to main content

Layout System Components

The template/components/layouts/ directory implements the item display layout system. It provides multiple layout variants (Classic, Grid, Cards, Masonry, Map) that can be switched at runtime via the layout switcher. Each layout receives item children and arranges them differently, with built-in sponsor ad injection.

Architecture Overview

Source Files

FileDescription
index.tsLayout registry and barrel exports
LayoutCards.tsxFlex-wrap card layout
LayoutClassic.tsxVertical list layout with sponsor injection
LayoutGrid.tsxResponsive CSS grid layout with sponsor injection
LayoutMasonry.tsxPinterest-style masonry layout with sponsor injection
LayoutMap.tsxInteractive map view layout

Layout Registry

The layoutComponents record maps layout keys to their components, enabling dynamic layout switching.

type LayoutKey = 'classic' | 'grid' | 'cards' | 'masonry';

const layoutComponents: Record<
LayoutKey,
({ children }: { children: React.ReactNode }) => JSX.Element
> = {
grid: LayoutGrid,
cards: LayoutCards,
classic: LayoutClassic,
masonry: LayoutMasonry,
};

Usage with Layout Switcher

import { layoutComponents, LayoutKey } from '@/components/layouts';

function ItemListing({ layout }: { layout: LayoutKey }) {
const LayoutComponent = layoutComponents[layout];

return (
<LayoutComponent>
{items.map(item => <ItemCard key={item.id} item={item} />)}
</LayoutComponent>
);
}

LayoutCards

The simplest layout -- a flex-wrap container with consistent gap spacing.

function LayoutCards({ children }: { children: ReactNode }): JSX.Element

CSS: flex flex-wrap gap-5

No sponsor injection is performed in this layout.

LayoutClassic

A vertical list layout (flex-col) with sponsor card injection after the third item.

function LayoutClassic({ children }: { children: ReactNode }): JSX.Element

Features:

  • Arranges items in a single column with gap-5
  • Injects a SponsorCard after position 3 (configurable via SPONSOR_INSERT_POSITION)
  • Only injects sponsors when available (reads from useSponsorAdsContext)

CSS: flex flex-col gap-5 max-w-full justify-items-stretch

LayoutGrid

A responsive CSS grid with fluid/fixed width modes and sponsor injection.

function LayoutGrid({ children }: { children: ReactNode }): JSX.Element

Features:

  • Uses useContainerWidth() to detect fluid vs fixed container mode
  • Responsive column counts:
    • Fixed: 1 -> 2 -> 2 -> 3 columns (sm -> lg -> xl)
    • Fluid: 1 -> 2 -> 3 -> 4 -> 6 -> 7 columns (sm -> lg -> xl -> 3xl -> 4xl)
  • Injects SponsorCard after position 3

LayoutMasonry

A Pinterest-style masonry layout using react-responsive-masonry with fluid/fixed configurations.

interface LayoutMasonryProps {
children: ReactNode;
}

function LayoutMasonry({ children }: LayoutMasonryProps): JSX.Element

Features:

  • Responsive column breakpoints for both fixed and fluid modes
  • Custom gutter breakpoints per viewport width
  • Sponsor card injection after position 3

Breakpoint Configuration (Fixed):

ViewportColumnsGutter
320px112px
640px212px
768px216px
1024px316px

Breakpoint Configuration (Fluid):

ViewportColumnsGutter
320px112px
640px212px
768px216px
1024px316px
1280px416px
1536px516px
1920px616px

LayoutMap

An interactive map view that renders items as markers using their geolocation data.

interface LayoutMapProps {
items: ItemData[];
}

function LayoutMap({ items }: LayoutMapProps): JSX.Element

Note: Unlike other layouts, LayoutMap does not use a children pattern. It takes an items prop directly and manages marker rendering internally.

Features:

  • Merges item data with coordinate data from useMapCoordinates
  • Displays interactive map with clustering enabled
  • Shows item count overlay
  • Renders a MapItemPopup when a marker is clicked
  • Falls back to a loading spinner or "no location data" message

Map States

Three layouts (Classic, Grid, Masonry) inject sponsor cards into the item list:

  1. Read sponsors from useSponsorAdsContext()
  2. Convert children to array via Children.toArray()
  3. Insert a <SponsorCard> at position 3 (after the first few items)
  4. Only inject if there are enough children and sponsors are available
const SPONSOR_INSERT_POSITION = 3;

const childrenWithSponsor = useMemo(() => {
const childArray = Children.toArray(children);
if (sponsors.length === 0 || childArray.length < SPONSOR_INSERT_POSITION) {
return childArray;
}
const result = [...childArray];
result.splice(SPONSOR_INSERT_POSITION, 0,
<SponsorCard sponsors={sponsors} rotationInterval={5000} />
);
return result;
}, [children, sponsors]);

Dependencies

  • react-responsive-masonry -- Masonry layout engine
  • @/components/ui/container -- useContainerWidth for fluid/fixed detection
  • @/components/sponsor-ads -- useSponsorAdsContext, SponsorCard
  • @/components/maps/map -- Map rendering component
  • @/hooks/use-map-coordinates -- Geolocation data fetching