fiddy/apps/web/components/bucket-card.tsx
2026-02-11 23:45:15 -08:00

152 lines
5.7 KiB
TypeScript

import { Bucket } from "@/lib/server/buckets";
import React from "react";
type BucketCardProps = {
bucket: Bucket;
icon?: string | null;
isExpanded: boolean;
toggleExpanded: (bucketId: number) => void;
isMenuOpen: boolean;
setMenuOpenId: React.Dispatch<React.SetStateAction<number | null>>;
setConfirmDeleteId: (bucketId: number) => void;
openEdit: (bucketId: number) => void;
limit: number;
usageLabel: string;
renderUsageBar: (bucket: Bucket) => React.ReactNode;
};
export function BucketCard({
bucket,
icon,
isExpanded,
toggleExpanded,
isMenuOpen,
setMenuOpenId,
setConfirmDeleteId,
openEdit,
limit,
usageLabel,
renderUsageBar,
}: BucketCardProps) {
return (
<div
className="w-full max-w-[360px] rounded-lg border border-accent-weak bg-panel px-3 py-3 transition hover:border-accent"
onClick={() => toggleExpanded(bucket.id)}
>
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-start gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-full border border-accent-weak bg-surface text-lg">
{icon || "🚫"}
</div>
<div className="min-w-0">
<div className="text-sm font-semibold truncate">{bucket.name}</div>
{bucket.description ? (
<div className={`text-xs text-soft ${isExpanded ? "" : "truncate"}`}>
{bucket.description}
</div>
) : null}
</div>
</div>
<div className="relative" data-bucket-menu>
<button
type="button"
className="rounded-lg btn-outline-accent px-2 py-1 text-xs"
onClick={(event) => {
event.stopPropagation();
setMenuOpenId((prev) => (prev === bucket.id ? null : bucket.id));
}}
aria-label="Bucket actions"
data-bucket-menu-button
>
</button>
{isMenuOpen ? (
<div className="absolute right-0 mt-2 w-40 rounded-lg border border-accent-weak bg-panel p-1 text-xs shadow-lg">
<button
type="button"
className="w-full rounded-md px-2 py-1 text-left hover:bg-accent-soft"
onClick={(event) => {
event.stopPropagation();
openEdit(bucket.id);
}}
>
Edit
</button>
<button
type="button"
className="w-full rounded-md px-2 py-1 text-left text-red-200 hover:bg-red-500/10"
onClick={(event) => {
event.stopPropagation();
setConfirmDeleteId(bucket.id);
}}
>
Delete
</button>
</div>
) : null}
</div>
</div>
{limit > 0 ? (
<>
{renderUsageBar(bucket)}
{isExpanded ? (
<div className="mt-2 space-y-2 text-xs text-soft">
<div>{usageLabel}</div>
<div className="flex flex-wrap gap-2">
{bucket.tags?.length ? (
bucket.tags.map((tag) => (
<span
key={tag}
className="rounded-full border border-accent-weak bg-accent-soft px-2 py-0.5 text-[11px]"
>
#{tag}
</span>
))
) : (
<span className="text-[11px] text-soft">No tags</span>
)}
</div>
</div>
) : null}
</>
) : isExpanded ? (
<div className="mt-2 flex flex-wrap gap-2 text-xs text-soft">
{bucket.tags?.length ? (
bucket.tags.map((tag) => (
<span
key={tag}
className="rounded-full border border-accent-weak bg-accent-soft px-2 py-0.5 text-[11px]"
>
#{tag}
</span>
))
) : (
<span className="text-[11px] text-soft">No tags</span>
)}
</div>
) : null}
</div>
);
}
export default React.memo(BucketCard, (prev, next) => (
prev.bucket === next.bucket
&& prev.icon === next.icon
&& prev.isExpanded === next.isExpanded
&& prev.isMenuOpen === next.isMenuOpen
&& prev.limit === next.limit
&& prev.usageLabel === next.usageLabel
));