"use client"; import { useRef } from "react"; import { FileText, Replace, Trash2, Undo2, Upload } from "lucide-react"; import { CertificationSide, CertificationFileInfo, CERTIFICATION_FILE_ACCEPT, CERTIFICATION_SIDE_LABELS, } from "@/lib/api/certifications"; import type { PendingCardImage } from "@/lib/certification-card-edits"; import { formatFileSize } from "@/lib/format"; import { Button } from "@/components/ui/button"; import { IconTooltip } from "@/components/ui/tooltip"; import { CertificationCardFrame, CertificationCardImage, } from "./certification-card-image"; /** A PDF is stored whole and never rendered - see `CertificationCardImage`. */ export const PDF_CONTENT_TYPE = "application/pdf"; interface CertificationCardSlotProps { side: CertificationSide; /** The uuid to fetch a stored image from, or "" on the create form. */ certificationUuid: string; /** What the API holds for this side today. */ stored?: CertificationFileInfo; /** Bytes the diver has picked, which the stored image gives way to. */ pending: PendingCardImage | null; /** True once the stored image is struck off, pending the save. */ isRemoved: boolean; disabled: boolean; onPick: (file: File) => void; onRemove: () => void; /** Takes back a removal, or drops a picked image. */ onUndo: () => void; } /** * One side of a card in the certification form: what the slot holds, what saving * will do to it, and the two controls that change either. * * Presentation only - it neither uploads nor crops. `CertificationCardFiles` owns * the picked bytes and the edit list, which is what makes "nothing is sent until * the form is saved" a property of one place rather than of two. */ export function CertificationCardSlot({ side, certificationUuid, stored, pending, isRemoved, disabled, onPick, onRemove, onUndo, }: CertificationCardSlotProps) { // Its own input, now that a slot is its own component: the picker has to be // opened from a Button, which cannot be a file input. const inputRef = useRef(null); const label = CERTIFICATION_SIDE_LABELS[side].toLowerCase(); const showsSomething = !!pending || (!!stored && !isRemoved); return (

{CERTIFICATION_SIDE_LABELS[side]}

{pending ? ( ) : ( )} { const file = event.target.files?.[0]; // Cleared unconditionally, so picking the *same* file again after a // cancelled crop still fires a change event. event.target.value = ""; if (file) onPick(file); }} ref={inputRef} />
{isRemoved ? ( ) : ( showsSomething && ( ) )}
{/* Every deferred state says so in words. The preview alone cannot: a slot showing a new picture and a slot showing the stored one look the same until the diver is told which is which. */}
); } /** * The picked bytes, drawn in the same frame the stored card will be. * * A PDF gets the labelled box `CertificationCardImage` gives a stored one, for the * reason that component explains: the app's CSP has no way to render one inline. * It carries no `previewUrl` for the same reason. */ function PendingCardPreview({ image, side, }: { image: PendingCardImage; side: CertificationSide; }) { if (image.blob.type === PDF_CONTENT_TYPE) { return ( PDF ); } return ( {image.previewUrl && ( // eslint-disable-next-line @next/next/no-img-element {`New )} ); } // What this slot will hold once the form is saved, in words. function SlotCaption({ pending, isRemoved, replacesStored, stored, }: { pending: PendingCardImage | null; isRemoved: boolean; replacesStored: boolean; stored?: CertificationFileInfo; }) { if (pending) { return (

{replacesStored ? "Replaces the stored image" : "Added"} when you save ·{" "} {formatFileSize(pending.blob.size)}

); } if (isRemoved) { return (

Deleted when you save

); } if (stored) { return (

{stored.original_filename} · {formatFileSize(stored.byte_size)}

); } return null; }