migration wip
This commit is contained in:
247
components/cards/BaseCard.tsx
Normal file
247
components/cards/BaseCard.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
'use client';
|
||||
|
||||
import React, { ReactNode, HTMLAttributes, forwardRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card } from '@/components/ui';
|
||||
|
||||
// Base card sizes
|
||||
export type CardSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
// Base card layouts
|
||||
export type CardLayout = 'vertical' | 'horizontal';
|
||||
|
||||
// Base card props interface
|
||||
export interface BaseCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
||||
/** Card title */
|
||||
title?: ReactNode;
|
||||
/** Card description/excerpt */
|
||||
description?: ReactNode;
|
||||
/** Card image URL */
|
||||
image?: string;
|
||||
/** Card image alt text */
|
||||
imageAlt?: string;
|
||||
/** Card size */
|
||||
size?: CardSize;
|
||||
/** Card layout */
|
||||
layout?: CardLayout;
|
||||
/** Card href/link */
|
||||
href?: string;
|
||||
/** Card badge/badges */
|
||||
badge?: ReactNode;
|
||||
/** Card footer content */
|
||||
footer?: ReactNode;
|
||||
/** Card header content */
|
||||
header?: ReactNode;
|
||||
/** Loading state */
|
||||
loading?: boolean;
|
||||
/** Hover effect */
|
||||
hoverable?: boolean;
|
||||
/** Card variant */
|
||||
variant?: 'elevated' | 'flat' | 'bordered';
|
||||
/** Image height */
|
||||
imageHeight?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
/** Children content */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
// Helper function to get size styles
|
||||
const getSizeStyles = (size: CardSize, layout: CardLayout) => {
|
||||
const sizeMap = {
|
||||
vertical: {
|
||||
sm: { container: 'max-w-xs', image: 'h-32', padding: 'p-3' },
|
||||
md: { container: 'max-w-sm', image: 'h-48', padding: 'p-4' },
|
||||
lg: { container: 'max-w-md', image: 'h-64', padding: 'p-6' },
|
||||
},
|
||||
horizontal: {
|
||||
sm: { container: 'max-w-sm', image: 'h-24 w-24', padding: 'p-3' },
|
||||
md: { container: 'max-w-lg', image: 'h-32 w-32', padding: 'p-4' },
|
||||
lg: { container: 'max-w-xl', image: 'h-40 w-40', padding: 'p-6' },
|
||||
},
|
||||
};
|
||||
|
||||
return sizeMap[layout][size];
|
||||
};
|
||||
|
||||
// Helper function to get image height
|
||||
const getImageHeight = (height: CardSize | 'sm' | 'md' | 'lg' | 'xl') => {
|
||||
const heightMap = {
|
||||
sm: 'h-32',
|
||||
md: 'h-48',
|
||||
lg: 'h-64',
|
||||
xl: 'h-80',
|
||||
};
|
||||
return heightMap[height] || heightMap['md'];
|
||||
};
|
||||
|
||||
// Skeleton loader component
|
||||
const CardSkeleton = ({ layout, size }: { layout: CardLayout; size: CardSize }) => {
|
||||
const sizeStyles = getSizeStyles(size, layout);
|
||||
|
||||
return (
|
||||
<div className={cn('animate-pulse bg-gray-200 rounded-lg', sizeStyles.container)}>
|
||||
<div className={cn('bg-gray-300 rounded-t-lg', sizeStyles.image)} />
|
||||
<div className={sizeStyles.padding}>
|
||||
<div className="h-6 bg-gray-300 rounded mb-2 w-3/4" />
|
||||
<div className="h-4 bg-gray-300 rounded mb-1 w-full" />
|
||||
<div className="h-4 bg-gray-300 rounded mb-1 w-5/6" />
|
||||
<div className="h-4 bg-gray-300 rounded w-2/3 mt-3" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main BaseCard Component
|
||||
export const BaseCard = forwardRef<HTMLDivElement, BaseCardProps>(
|
||||
(
|
||||
{
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
imageAlt = '',
|
||||
size = 'md',
|
||||
layout = 'vertical',
|
||||
href,
|
||||
badge,
|
||||
footer,
|
||||
header,
|
||||
loading = false,
|
||||
hoverable = true,
|
||||
variant = 'elevated',
|
||||
imageHeight,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const sizeStyles = getSizeStyles(size, layout);
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return <CardSkeleton layout={layout} size={size} />;
|
||||
}
|
||||
|
||||
// Content sections
|
||||
const renderImage = () => {
|
||||
if (!image) return null;
|
||||
|
||||
const imageClasses = layout === 'horizontal'
|
||||
? cn('flex-shrink-0 overflow-hidden rounded-lg', sizeStyles.image)
|
||||
: cn('w-full overflow-hidden rounded-t-lg', imageHeight ? getImageHeight(imageHeight) : sizeStyles.image);
|
||||
|
||||
return (
|
||||
<div className={imageClasses}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
if (!header && !badge) return null;
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
{header}
|
||||
{badge}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!title) return null;
|
||||
return (
|
||||
<h3 className={cn(
|
||||
'font-semibold text-gray-900 leading-tight',
|
||||
size === 'sm' && 'text-base',
|
||||
size === 'md' && 'text-lg',
|
||||
size === 'lg' && 'text-xl'
|
||||
)}>
|
||||
{title}
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDescription = () => {
|
||||
if (!description) return null;
|
||||
return (
|
||||
<div className={cn(
|
||||
'text-gray-600 mt-1',
|
||||
size === 'sm' && 'text-sm',
|
||||
size === 'md' && 'text-sm',
|
||||
size === 'lg' && 'text-base'
|
||||
)}>
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFooter = () => {
|
||||
if (!footer) return null;
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-gray-100">
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Card content
|
||||
const cardContent = (
|
||||
<div className={cn(
|
||||
'flex',
|
||||
layout === 'horizontal' && 'flex-row',
|
||||
layout === 'vertical' && 'flex-col',
|
||||
sizeStyles.padding
|
||||
)}>
|
||||
{layout === 'horizontal' && renderImage()}
|
||||
<div className={cn('flex-1', layout === 'horizontal' && 'ml-4')}>
|
||||
{renderHeader()}
|
||||
{renderTitle()}
|
||||
{renderDescription()}
|
||||
{children}
|
||||
{renderFooter()}
|
||||
</div>
|
||||
{layout === 'vertical' && renderImage()}
|
||||
</div>
|
||||
);
|
||||
|
||||
// If href is provided, wrap in a Next.js Link
|
||||
if (href) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
'group block',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
hoverable && 'hover:-translate-y-1 hover:shadow-xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Card variant={variant} padding="none" className={sizeStyles.container}>
|
||||
{cardContent}
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, just return the card
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
padding="none"
|
||||
className={cn(sizeStyles.container, className)}
|
||||
hoverable={hoverable}
|
||||
{...props}
|
||||
>
|
||||
{cardContent}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BaseCard.displayName = 'BaseCard';
|
||||
Reference in New Issue
Block a user