All files / src/features/collections PhotoPoolPreview.tsx

75% Statements 3/4
100% Branches 4/4
66.66% Functions 2/3
75% Lines 3/4

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                                                      4x   5x                           4x                                                                                      
import { Card, CardImage } from "@/components/ui";
import { getPoolPhotoAssetUrl, PHOTO_WIDTH } from "@/lib/photos";
import { quietLink } from "@/lib/design/quiet-link";
import Link from "next/link";
 
interface PhotoPoolPreviewProps {
  /** Resolved collection id — used only to build the guarded pool /file URLs. */
  collectionId: string;
  /** Slug-or-id path segment for the /pool route. */
  poolHref: string;
  /**
   * Bounded preview rows (≤6) from findPoolByCollection(collectionId, 6).
   * Only `id` is consumed — passing the minimal shape keeps blobKey/uploadedBy
   * off any future client serialization boundary (defensive, Security LOW).
   */
  photos: { id: string }[];
  /** Show the quiet Make items link (sorting holds the page's primary action). */
  showLink: boolean;
}
 
// Read-only strip of loose pool photos above the home grid. Its Make items link shows
// only when sorting holds the page's primary action; otherwise the bar's primary is
// Make items itself (one name on screen once) and the thumbnails are a single link to
// the pool. The thumbnails are decorative (alt=""): the link carries the meaning, so
// AT isn't read 4–6 filenames (spec §11).
export function PhotoPoolPreview({ collectionId, poolHref, photos, showLink }: PhotoPoolPreviewProps) {
  const thumbnails = (
    <ul className="grid grid-cols-4 gap-2 sm:grid-cols-6">
      {photos.map((photo) => (
        <li
          key={photo.id}
          className="relative flex aspect-square items-center justify-center overflow-hidden rounded-control bg-content-soft"
        >
          <CardImage
            src={getPoolPhotoAssetUrl(collectionId, photo.id, { w: PHOTO_WIDTH.poolTile })}
            alt=""
            fallback={<NeutralThumb />}
          />
        </li>
      ))}
    </ul>
  );
 
  return (
    <section aria-labelledby="photo-pool-heading">
      <Card padding="lg" className="space-y-4">
        <div>
          <h2 id="photo-pool-heading" className="text-xl text-ink">
            Photos waiting
          </h2>
          <p className="mt-1 text-sm text-ink-soft">These can become items whenever you&rsquo;re ready.</p>
        </div>
        {showLink ? (
          thumbnails
        ) : (
          // Photos-only: the bar's primary already reads "Make items", so the strip is one
          // link to the pool named by the heading — the visible name stays on screen once.
          <Link
            href={poolHref}
            aria-labelledby="photo-pool-heading"
            className="block rounded-control focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-primary"
          >
            {thumbnails}
          </Link>
        )}
        {showLink && (
          <Link href={poolHref} className={quietLink}>
            Make items
          </Link>
        )}
      </Card>
    </section>
  );
}
 
// Calm placeholder for a missing/failed pool thumbnail. Unlike ItemCard's
// fallback there is no item yet, so there is no initial — a neutral mark only.
function NeutralThumb() {
  return (
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-6 w-6 text-ink-soft">
      <rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" strokeWidth="1.5" />
      <circle cx="8.5" cy="10" r="1.5" fill="currentColor" />
      <path d="M5 17l4.5-4.5a2 2 0 0 1 2.8 0L19 19" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}