From f486a659d08ce90ed525a012b117841582c4ceca Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 22 Oct 2025 14:49:05 +0700 Subject: [PATCH] feat(FE-114): add Card component with customizable layout and styling options --- src/components/Card.tsx | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/components/Card.tsx diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 00000000..ba573dfb --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { + HTMLAttributes, + ReactNode, +} from 'react'; + +import { cn } from '@/lib/helper'; + +export interface CardProps extends Omit, 'className'> { + title?: string; + subtitle?: string; + image?: string; + imageAlt?: string; + actions?: ReactNode; + footer?: ReactNode; + className?: { + wrapper?: string; + image?: string; + body?: string; + title?: string; + subtitle?: string; + actions?: string; + footer?: string; + }; + variant?: 'default' | 'compact' | 'bordered' | 'shadow' | 'image-full'; + size?: 'sm' | 'md' | 'lg'; +} + +const Card = ({ + title, + subtitle, + image, + imageAlt, + actions, + footer, + className, + variant = 'default', + size = 'md', + children, + ...props +}: CardProps) => { + const getCardClasses = () => { + const baseClasses = 'card bg-base-100'; + + const variantClasses = { + 'default': '', + 'compact': 'card-compact', + 'bordered': 'border border-base-300', + 'shadow': 'shadow-xl', + 'image-full': 'card-side card-compact shadow-xl', + }; + + const sizeClasses = { + 'sm': 'w-64', + 'md': 'w-96', + 'lg': 'w-[28rem]', + }; + + return cn( + baseClasses, + variantClasses[variant], + variant !== 'image-full' ? sizeClasses[size] : '', + className?.wrapper + ); + }; + + const getImageClasses = () => { + if (variant === 'image-full') { + return cn('w-32 h-32 object-cover', className?.image); + } + return cn('h-48 object-cover', className?.image); + }; + + const getBodyClasses = () => { + const baseClasses = 'card-body'; + + if (variant === 'compact' || variant === 'image-full') { + return cn(baseClasses, 'p-4', className?.body); + } + + return cn(baseClasses, 'p-6', className?.body); + }; + + const getTitleClasses = () => { + const sizeClasses = { + 'sm': 'text-lg', + 'md': 'text-xl', + 'lg': 'text-2xl', + }; + + return cn('card-title font-bold', sizeClasses[size], className?.title); + }; + + const getSubtitleClasses = () => { + return cn('text-base-content/70 text-sm mt-1', className?.subtitle); + }; + + const getActionsClasses = () => { + return cn('card-actions justify-end mt-4', className?.actions); + }; + + const getFooterClasses = () => { + return cn('border-t border-base-300 mt-4 pt-4', className?.footer); + }; + + if (variant === 'image-full' && image) { + return ( +
+
+ {imageAlt +
+
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} + {children} + {actions &&
{actions}
} +
+ {footer &&
{footer}
} +
+ ); + } + + return ( +
+ {image && ( +
+ {imageAlt +
+ )} +
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} + {children} + {actions &&
{actions}
} +
+ {footer &&
{footer}
} +
+ ); +}; + +export default Card; \ No newline at end of file