Files
klz-cables.com/components/cards/CardGrid.tsx
2025-12-29 18:18:48 +01:00

192 lines
4.4 KiB
TypeScript

'use client';
import React, { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { LoadingSkeleton } from '@/components/ui';
// CardGrid column options
export type GridColumns = 1 | 2 | 3 | 4;
// CardGrid gap options
export type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
// CardGrid props interface
export interface CardGridProps {
/** Card items to render */
items?: ReactNode[];
/** Number of columns */
columns?: GridColumns;
/** Gap spacing */
gap?: GridGap;
/** Loading state */
loading?: boolean;
/** Empty state message */
emptyMessage?: string;
/** Empty state component */
emptyComponent?: ReactNode;
/** Loading skeleton count */
skeletonCount?: number;
/** Additional classes */
className?: string;
/** Children (alternative to items) */
children?: ReactNode;
}
// Helper function to get gap classes
const getGapClasses = (gap: GridGap): string => {
const gapMap = {
xs: 'gap-2',
sm: 'gap-4',
md: 'gap-6',
lg: 'gap-8',
xl: 'gap-12',
};
return gapMap[gap];
};
// Helper function to get column classes
const getColumnClasses = (columns: GridColumns): string => {
const columnMap = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
};
return columnMap[columns];
};
// Skeleton loader component
const GridSkeleton = ({
count,
columns,
gap
}: {
count: number;
columns: GridColumns;
gap: GridGap;
}) => {
const gapClasses = getGapClasses(gap);
const columnClasses = getColumnClasses(columns);
return (
<div className={cn('grid', columnClasses, gapClasses)}>
{Array.from({ length: count }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="bg-gray-200 rounded-lg h-64" />
</div>
))}
</div>
);
};
// Empty state component
const EmptyState = ({
message,
customComponent
}: {
message?: string;
customComponent?: ReactNode;
}) => {
if (customComponent) {
return <div className="text-center py-12">{customComponent}</div>;
}
return (
<div className="text-center py-12">
<div className="text-gray-400 text-lg mb-2">
{message || 'No items to display'}
</div>
<div className="text-gray-300 text-sm">
{message ? '' : 'Try adjusting your filters or check back later'}
</div>
</div>
);
};
export const CardGrid: React.FC<CardGridProps> = ({
items,
columns = 3,
gap = 'md',
loading = false,
emptyMessage,
emptyComponent,
skeletonCount = 6,
className = '',
children,
}) => {
// Use children if provided, otherwise use items
const content = children || items;
// Loading state
if (loading) {
return (
<GridSkeleton
count={skeletonCount}
columns={columns}
gap={gap}
/>
);
}
// Empty state
if (!content || (Array.isArray(content) && content.length === 0)) {
return (
<EmptyState
message={emptyMessage}
customComponent={emptyComponent}
/>
);
}
// Render grid
const gapClasses = getGapClasses(gap);
const columnClasses = getColumnClasses(columns);
return (
<div className={cn('grid', columnClasses, gapClasses, className)}>
{Array.isArray(content)
? content.map((item, index) => (
<div key={index} className="contents">
{item}
</div>
))
: content
}
</div>
);
};
// Grid variations
export const CardGrid2: React.FC<CardGridProps> = (props) => (
<CardGrid {...props} columns={2} />
);
export const CardGrid3: React.FC<CardGridProps> = (props) => (
<CardGrid {...props} columns={3} />
);
export const CardGrid4: React.FC<CardGridProps> = (props) => (
<CardGrid {...props} columns={4} />
);
// Responsive grid with auto columns
export const CardGridAuto: React.FC<CardGridProps> = ({
gap = 'md',
className = '',
...props
}) => {
const gapClasses = getGapClasses(gap);
return (
<div className={cn('grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4', gapClasses, className)}>
{props.items && Array.isArray(props.items)
? props.items.map((item, index) => (
<div key={index} className="contents">
{item}
</div>
))
: props.children
}
</div>
);
};