All files / src/features/items ItemCard.tsx

100% Statements 2/2
100% Branches 11/11
100% Functions 1/1
100% Lines 2/2

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93                                              10x                                     138x                                                                                                    
import type { Item } from "@/types";
import type { UsageAnswer } from "@/lib/derive-usage";
import { hasName, itemDisplayName, itemInitial } from "@/lib/items";
import { usageChipLabel } from "@/features/items/usage-chip-copy";
import { BUCKET_LABELS } from "@/features/sorting/roundCopy";
import { Badge, Card, CardImage, PhotoViewControl, type ViewerPhoto } from "@/components/ui";
import Link from "next/link";
 
interface ItemCardProps {
  item: Item;
  collectionId: string;
  heroPhotoUrl?: string;
  bucket?: "top" | "high" | "medium" | "low";
  /** Override the image area sizing (default a square). Triage caps height so
   *  the decision buttons stay above the fold. */
  imageClassName?: string;
  /** When set, a "view photo" control opens the zoom/rotate lightbox for the hero. */
  viewerPhoto?: ViewerPhoto;
  /** Epic S1: a quiet, count-free usage note ("In your life" / "Resting").
   *  Absent (undefined) when this item's usage is undecided — no chip renders. */
  usage?: UsageAnswer;
}
 
const bucketDots = {
  top: "●●●●",
  high: "●●●",
  medium: "●●",
  low: "●",
};
 
 
export function ItemCard({
  item,
  collectionId,
  heroPhotoUrl,
  bucket,
  imageClassName = "aspect-square",
  viewerPhoto,
  usage,
}: ItemCardProps) {
  // Stretched-link: the navigating <Link> is an overlay sibling so the view
  // control is valid DOM. `isolate` keeps the control (z-20) above the link (z-10).
  return (
    <Card interactive padding="none" className="relative isolate flex h-full flex-col overflow-hidden">
      <div className={`bg-content-soft ${imageClassName} relative flex items-center justify-center overflow-hidden`}>
        <CardImage
          src={heroPhotoUrl}
          alt={itemDisplayName(item)}
          fallback={
            <span className="font-display text-5xl text-ink-soft" aria-hidden>
              {itemInitial(item)}
            </span>
          }
        />
      </div>
      <div className="flex flex-1 flex-col justify-between p-3">
        <div className="space-y-2">
          {/* Wraps the chip under the name when the tile is too narrow (phone) rather
              than squeezing the name to a few letters. */}
          <div className="flex flex-wrap items-start justify-between gap-2">
            <h3 className="min-w-0 grow basis-24 text-ink line-clamp-2 break-words">{itemDisplayName(item)}</h3>
            {bucket && (
              <Badge variant={bucket} title={BUCKET_LABELS[bucket]} className="shrink-0">
                <span role="img" aria-label={BUCKET_LABELS[bucket]}>{bucketDots[bucket]}</span>
              </Badge>
            )}
          </div>
          {hasName(item) ? (
            item.brand && <p className="text-xs text-ink-soft truncate">{item.brand}</p>
          ) : (
            <p className="text-xs text-ink-soft truncate">Add a name</p>
          )}
          {/* Epic S1 usage chip: a quiet, count-free note. Meaning is carried by
              the words (not color), and the neutral "active" variant is AA in
              both themes. No chip when usage is undecided (undefined). */}
          {usage && (
            <Badge variant="active" className="w-fit text-xs">
              {usageChipLabel(usage)}
            </Badge>
          )}
        </div>
      </div>
 
      <Link
        href={`/collections/${collectionId}/items/${item.number}`}
        className="absolute inset-0 z-10"
        aria-label={itemDisplayName(item)}
      />
      {viewerPhoto && <PhotoViewControl photo={viewerPhoto} className="absolute right-2 top-2 z-20" />}
    </Card>
  );
}