migration wip
This commit is contained in:
265
components/ui/Card.tsx
Normal file
265
components/ui/Card.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user