"use client"; import Link from "next/link"; import { Download } from "lucide-react"; import { coursesAPI, Course } from "@/lib/api/courses"; import { contactsAPI } from "@/lib/api/contacts"; import { peopleAPI } from "@/lib/api/people"; import { useRecordByUuid } from "@/hooks/useRecordByUuid"; import { useWithReturnTo } from "@/hooks/useReturnTo"; import { certificationFilePath, certificationsAPI, certificationAgencyLabel, certificationFile, Certification, CertificationSide, CERTIFICATION_SIDES, CERTIFICATION_SIDE_LABELS, } from "@/lib/api/certifications"; import { certificationExpiryBadgeVariant, certificationExpiryLabel, certificationExpiryStatus, } from "@/lib/certification"; import { formatDateOnly } from "@/lib/date-time"; import { getApiErrorMessage } from "@/lib/api/error"; import { saveApiDownload } from "@/lib/download"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ButtonSpinner } from "@/components/ui/button-spinner"; import { useToast } from "@/components/ui/use-toast"; import { CertificationCardImage } from "./certification-card-image"; import { useEffect, useState } from "react"; interface CertificationViewDialogProps { certification: Certification | null; onOpenChange: (open: boolean) => void; } function DetailRow({ label, value, }: { label: string; value: string | null | undefined; }) { if (!value) return null; return (
{label}
{value}
); } // The course this card came out of, as a link to it. // // The certification carries the course's uuid and nothing else, so the name has // to be fetched - and the row only appears once it has arrived. A failed lookup // is non-fatal and leaves the row out, exactly as the dive page's trip link does: // the rest of the dialog is what the diver opened it for. function CourseRow({ courseUuid }: { courseUuid: string }) { const [course, setCourse] = useState(null); const withReturnTo = useWithReturnTo(); useEffect(() => { let cancelled = false; coursesAPI .getCourse(courseUuid) .then((data) => { if (!cancelled) setCourse(data); }) .catch((error) => console.error("Failed to fetch course:", error)); return () => { cancelled = true; }; }, [courseUuid]); if (!course) return null; return (
Course
{course.name}
); } // Who ran the course the card came out of, on `CourseRow`'s terms: the // certification carries the contact's uuid and nothing else, so the row appears // once the name has arrived, and a failed lookup leaves it out. function ContactRow({ contactUuid }: { contactUuid: string }) { const contact = useRecordByUuid(contactsAPI.getContact, contactUuid); if (!contact) return null; return (
Dive center
{contact.name}
); } // Who signed the card, on the same terms: a person, by uuid, named once the // record has arrived and linked to their page. function InstructorRow({ personUuid }: { personUuid: string }) { const person = useRecordByUuid(peopleAPI.getPerson, personUuid); const withReturnTo = useWithReturnTo(); if (!person) return null; return (
Instructor
{person.name}
); } // Read-only view of a certification and its card images - what a diver opens at // a dive shop counter. Large images, details underneath. export function CertificationViewDialog({ certification, onOpenChange, }: CertificationViewDialogProps) { const { toast } = useToast(); const [downloadingSide, setDownloadingSide] = useState(null); if (!certification) return null; const expiry = certificationExpiryStatus(certification.expires_on); // Downloading goes through `saveApiDownload` for the same reason rendering goes // through the API client: the endpoint needs an `Authorization` header, so a // plain `` to it would 401. See `lib/download.ts`. const handleDownload = async (side: CertificationSide) => { const file = certificationFile(certification, side); if (!file) return; try { setDownloadingSide(side); await saveApiDownload( certificationFilePath(certification.uuid, side), async () => ({ blob: await certificationsAPI.getCertificationFileBlob( certification.uuid, side, ), filename: file.original_filename, }), ); } catch (error) { toast({ title: "Error", description: getApiErrorMessage( error, "Failed to download the file. Please try again.", ), variant: "destructive", }); } finally { setDownloadingSide(null); } }; return ( {certification.name} {certificationAgencyLabel( certification.agency, certification.agency_other, )} {expiry && ( {certificationExpiryLabel(expiry)} )}
{CERTIFICATION_SIDES.map((side) => { const file = certificationFile(certification, side); return (

{CERTIFICATION_SIDE_LABELS[side]}

{file && ( )}
); })}
{certification.course_uuid && ( )} {certification.contact_uuid && ( )} {certification.instructor_uuid && ( )}
{certification.notes && (

Notes

{certification.notes}

)}
); }