"use client"; import { useCallback, type ReactNode } from "react"; import { FileText, ImageOff } from "lucide-react"; import { certificationsAPI, CertificationFileInfo, CertificationSide, } from "@/lib/api/certifications"; import { useAuthedBlobUrl } from "@/hooks/useAuthedBlobUrl"; import { getApiErrorMessage } from "@/lib/api/error"; import { shrinkPicture } from "@/lib/shrink-picture"; import { CERTIFICATION_CARD_ASPECT_CLASS, certificationFileVersion, } from "@/lib/certification"; import { cn } from "@/lib/utils"; import { ButtonSpinner } from "@/components/ui/button-spinner"; interface CertificationCardFrameProps { /** * Draws the frame at a thumbnail's width instead of filling its column. It * changes the *width* only: the shape is the same everywhere, which is the * whole point of the frame existing. */ compact?: boolean; /** Dashed, for a slot with nothing in it. */ empty?: boolean; className?: string; /** Announces what the frame stands for when it holds no picture to describe. */ "aria-label"?: string; title?: string; children?: ReactNode; } /** * The box a c-card is drawn in, wherever one is drawn: the standard card shape, * the app's own corner radius, and `overflow-hidden` so a picture filling it is * clipped to those corners rather than sitting square inside them. * * Exported because a card is not always a picture - a stored PDF, a slot the diver * has not filled, a fetch that failed and the check-in sheet's own PDF note all * occupy the same footprint, and one box is what keeps them to one size. */ export function CertificationCardFrame({ compact = false, empty = false, className, children, ...rest }: CertificationCardFrameProps) { return (
img]:absolute [&>img]:inset-0 [&>img]:size-full [&>img]:object-cover", CERTIFICATION_CARD_ASPECT_CLASS, compact ? "w-20" : "w-full", empty && "border-dashed", className, )} {...rest} > {children}
); } interface CertificationCardImageProps { certificationUuid: string; side: CertificationSide; // The embedded metadata for this side, or undefined if it has no file. Passing // it in (rather than probing the API) is what lets a list of cards render // without a request per empty slot. file?: CertificationFileInfo; className?: string; // Renders the "no image" state as a compact placeholder instead of a full // dashed panel, for a small thumbnail such as the check-in sheet's. compact?: boolean; // Redraws the picture at no more than this many pixels on its long edge - see // `shrinkPicture`. The stored file as it is when unset. maxEdge?: number; } // Renders one side of a stored certification card. // // Images are fetched with the API client and shown from an object URL, because // the endpoint is owner-only and an `` cannot carry an `Authorization` // header - see `hooks/useAuthedBlobUrl.ts`. // // PDFs are deliberately *not* previewed. Rendering one inline would mean an // `` or `