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

265 lines
6.3 KiB
TypeScript

import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
import { cn } from '../../lib/utils';
// Card variants
type CardVariant = 'elevated' | 'flat' | 'bordered';
// Card props interface
interface CardProps extends HTMLAttributes<HTMLDivElement> {
variant?: CardVariant;
children?: ReactNode;
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
hoverable?: boolean;
shadow?: boolean;
}
// Card header props
interface CardHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
title?: ReactNode;
subtitle?: ReactNode;
icon?: ReactNode;
action?: ReactNode;
}
// Card body props
interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
}
// Card footer props
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
align?: 'left' | 'center' | 'right';
}
// Card image props
interface CardImageProps extends HTMLAttributes<HTMLDivElement> {
src: string;
alt?: string;
height?: 'sm' | 'md' | 'lg' | 'xl';
position?: 'top' | 'background';
}
// Helper function to get variant styles
const getVariantStyles = (variant: CardVariant) => {
switch (variant) {
case 'elevated':
return 'bg-white shadow-lg shadow-gray-200/50 border border-gray-100';
case 'flat':
return 'bg-white shadow-sm border border-gray-100';
case 'bordered':
return 'bg-white border-2 border-gray-200';
default:
return 'bg-white shadow-md border border-gray-100';
}
};
// Helper function to get padding styles
const getPaddingStyles = (padding: CardProps['padding']) => {
switch (padding) {
case 'none':
return '';
case 'sm':
return 'p-3';
case 'md':
return 'p-4';
case 'lg':
return 'p-6';
case 'xl':
return 'p-8';
default:
return 'p-4';
}
};
// Helper function to get image height
const getImageHeight = (height: CardImageProps['height']) => {
switch (height) {
case 'sm':
return 'h-32';
case 'md':
return 'h-48';
case 'lg':
return 'h-64';
case 'xl':
return 'h-80';
default:
return 'h-48';
}
};
// Main Card Component
export const Card = forwardRef<HTMLDivElement, CardProps>(
(
{
variant = 'elevated',
padding = 'md',
hoverable = false,
shadow = true,
className = '',
children,
...props
},
ref
) => {
return (
<div
ref={ref}
className={cn(
'rounded-lg',
'transition-all duration-200 ease-in-out',
// Variant styles
getVariantStyles(variant),
// Padding
getPaddingStyles(padding),
// Hover effect
hoverable && 'hover:shadow-xl hover:shadow-gray-200/70 hover:-translate-y-1',
// Shadow override
!shadow && 'shadow-none',
// Custom classes
className
)}
{...props}
>
{children}
</div>
);
}
);
Card.displayName = 'Card';
// Card Header Component
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
({ title, subtitle, icon, action, className = '', children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
'flex items-start justify-between gap-4',
'border-b border-gray-100 pb-4 mb-4',
className
)}
{...props}
>
<div className="flex items-start gap-3 flex-1">
{icon && <div className="text-gray-500 mt-0.5">{icon}</div>}
<div className="flex-1">
{title && (
<div className="text-lg font-semibold text-gray-900 leading-tight">
{title}
</div>
)}
{subtitle && (
<div className="text-sm text-gray-600 mt-1 leading-relaxed">
{subtitle}
</div>
)}
{children}
</div>
</div>
{action && <div className="flex-shrink-0">{action}</div>}
</div>
);
}
);
CardHeader.displayName = 'CardHeader';
// Card Body Component
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
({ className = '', children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn('space-y-3', className)}
{...props}
>
{children}
</div>
);
}
);
CardBody.displayName = 'CardBody';
// Card Footer Component
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
({ align = 'left', className = '', children, ...props }, ref) => {
const alignmentClasses = {
left: 'justify-start',
center: 'justify-center',
right: 'justify-end',
};
return (
<div
ref={ref}
className={cn(
'flex items-center gap-3',
'border-t border-gray-100 pt-4 mt-4',
alignmentClasses[align],
className
)}
{...props}
>
{children}
</div>
);
}
);
CardFooter.displayName = 'CardFooter';
// Card Image Component
export const CardImage = forwardRef<HTMLDivElement, CardImageProps>(
({ src, alt, height = 'md', position = 'top', className = '', ...props }, ref) => {
const heightClasses = getImageHeight(height);
if (position === 'background') {
return (
<div
ref={ref}
className={cn(
'relative w-full overflow-hidden rounded-t-lg',
heightClasses,
className
)}
{...props}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={alt || ''}
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
</div>
);
}
return (
<div
ref={ref}
className={cn(
'w-full overflow-hidden rounded-t-lg',
heightClasses,
className
)}
{...props}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={alt || ''}
className="w-full h-full object-cover"
/>
</div>
);
}
);
CardImage.displayName = 'CardImage';
// Export types for external use
export type { CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, CardImageProps, CardVariant };