122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import type { MouseEventHandler, ReactNode } from 'react';
|
|
import Card from './Card';
|
|
|
|
interface PresetCardStat {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface PresetCardProps {
|
|
title: string;
|
|
subtitle?: string;
|
|
primaryTag?: string;
|
|
description?: string;
|
|
stats?: PresetCardStat[];
|
|
selected?: boolean;
|
|
disabled?: boolean;
|
|
onSelect?: () => void;
|
|
className?: string;
|
|
children?: ReactNode;
|
|
}
|
|
|
|
export default function PresetCard({
|
|
title,
|
|
subtitle,
|
|
primaryTag,
|
|
description,
|
|
stats,
|
|
selected,
|
|
disabled,
|
|
onSelect,
|
|
className = '',
|
|
children,
|
|
}: PresetCardProps) {
|
|
const isInteractive = typeof onSelect === 'function' && !disabled;
|
|
|
|
const handleClick: MouseEventHandler<HTMLButtonElement | HTMLDivElement> = (event) => {
|
|
if (!isInteractive) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
onSelect?.();
|
|
};
|
|
|
|
const baseBorder = selected ? 'border-primary-blue' : 'border-charcoal-outline';
|
|
const baseBg = selected ? 'bg-primary-blue/10' : 'bg-iron-gray';
|
|
const baseRing = selected ? 'ring-2 ring-primary-blue/40' : '';
|
|
const disabledClasses = disabled ? 'opacity-60 cursor-not-allowed' : '';
|
|
const hoverClasses = isInteractive && !disabled ? 'hover:bg-iron-gray/80 hover:scale-[1.01]' : '';
|
|
|
|
const content = (
|
|
<div className="flex h-full flex-col gap-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="text-sm font-semibold text-white">{title}</div>
|
|
{subtitle && (
|
|
<div className="mt-0.5 text-xs text-gray-400">{subtitle}</div>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col items-end gap-1">
|
|
{primaryTag && (
|
|
<span className="inline-flex rounded-full bg-primary-blue/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary-blue">
|
|
{primaryTag}
|
|
</span>
|
|
)}
|
|
{selected && (
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary-blue/10 px-2 py-0.5 text-[10px] font-medium text-primary-blue">
|
|
<span className="h-1.5 w-1.5 rounded-full bg-primary-blue" />
|
|
Selected
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{description && (
|
|
<p className="text-xs text-gray-300">{description}</p>
|
|
)}
|
|
|
|
{children}
|
|
|
|
{stats && stats.length > 0 && (
|
|
<div className="mt-1 border-t border-charcoal-outline/70 pt-2">
|
|
<dl className="grid grid-cols-1 gap-2 text-[11px] text-gray-400 sm:grid-cols-3">
|
|
{stats.map((stat) => (
|
|
<div key={stat.label} className="space-y-0.5">
|
|
<dt className="font-medium text-gray-500">{stat.label}</dt>
|
|
<dd className="text-xs text-gray-200">{stat.value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const commonClasses = `${baseBorder} ${baseBg} ${baseRing} ${hoverClasses} ${disabledClasses} ${className}`;
|
|
|
|
if (isInteractive) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={handleClick as MouseEventHandler<HTMLButtonElement>}
|
|
disabled={disabled}
|
|
className={`group block w-full rounded-lg text-left text-sm shadow-card outline-none transition-all duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-blue ${commonClasses}`}
|
|
>
|
|
<div className="p-4">
|
|
{content}
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
className={commonClasses}
|
|
onClick={handleClick as MouseEventHandler<HTMLDivElement>}
|
|
>
|
|
{content}
|
|
</Card>
|
|
);
|
|
} |