192 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}; |