"use client"; import type { ReactNode } from "react"; import Link from "next/link"; import { Dive } from "@/lib/api/dives"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DeleteMenuItem } from "@/components/ui/item-actions-menu"; import { BACKDROP_CARD_LINK, BackdropCard, BackdropCardFigures, UnplacedBackdrop, type BackdropCardFigure, } from "@/components/ui/backdrop-card"; import { MapBackdrop } from "@/components/map/map-backdrop"; import { DiveTitle } from "@/components/dives/dive-title"; import { diveFacts } from "@/components/dives/dive-facts"; import { diveChips } from "@/components/dives/dive-chips"; import { FactsLine } from "@/components/ui/icon-fact"; import { diveMapLocations } from "@/components/dives/dive-map-locations"; import { DiveIcon } from "@/components/logo"; import { DiveProfileSilhouette } from "@/components/dives/dive-profile-silhouette"; import { formatDurationHoursMinutes } from "@/lib/date-time"; import { useUnits } from "@/hooks/useUnits"; import { useWithReturnTo } from "@/hooks/useReturnTo"; import { formatDepth, formatTemperature } from "@/lib/units"; import { cn } from "@/lib/utils"; import { Edit } from "lucide-react"; // The profile's band at the foot of the backdrop, just clear of the name: the // `h-14` below. const SILHOUETTE_HEIGHT = 56; const SILHOUETTE_GAP = 4; // The chips' row, over the band's foot: a chip at its widest breakpoint and the // `bottom-1` it stands on. Lower than the profile's band, which covers it where // a dive has both. const CHIPS_HEIGHT = 26; interface DiveCardProps { dive: Dive; // Offers Delete beside Edit. Left out where the list has no delete of its own // to run. onDelete?: () => void; isDeleting?: boolean; // The control that adds a dive the trip page shows but the trip does not hold // yet. A card handed one is drawn muted, with it in place of the menu. addToTrip?: ReactNode; // A dive of an import rather than of the log, which has no menu: it may not // exist yet. importRow?: DiveCardImportRow; } export interface DiveCardImportRow { // Stands in the card's corner, where the menu would. chip: ReactNode; // The dive the card links to, which an import's row names apart from the dive // it shows; left out, the card links to nothing. linkTo?: string; // Makes the card a button in place of its link. open?: { onOpen: () => void; label: string }; } // One dive as a card, in every list of dives: a map of its sites and fixes as // the backdrop - the map's water where it has none - with // its depth curve across the foot of it, and its dive time, deepest point and // water temperature - or, where it has no temperature, the species it saw. export function DiveCard({ dive, onDelete, isDeleting, addToTrip, importRow, }: DiveCardProps) { const units = useUnits(); // The dive's page and its edit form both return to the page the card is on. const withReturnTo = useWithReturnTo(); const outline = dive.depth_outline; // Whole units, as the dive page's hero rounds them: the second decimal is not // what anyone reads a dive's depth for. `!= null`, since 0 m is a reading. const depth = (meters?: number) => meters != null ? formatDepth(meters, units, { decimals: 0 }) : "-"; const figures: BackdropCardFigure[] = [ { label: "Dive time", value: formatDurationHoursMinutes(dive.submerged_time), }, { label: "Max depth", value: depth(dive.max_depth) }, ]; // `!= null`, since 0 °C is a reading. if (dive.bottom_temperature != null) { figures.push({ label: "Water temp", value: formatTemperature(dive.bottom_temperature, units, { decimals: 0, }), }); } // Only into a spare slot: the card's row holds three figures. An import's row // carries the dive's detail shape, which has the sightings and not their count. const speciesCount = dive.species_count ?? dive.sightings?.length; if (figures.length < 3 && speciesCount) { figures.push({ label: "Species seen", value: speciesCount }); } const chips = diveChips(dive); const title = ( ); const linkTo = importRow ? importRow.linkTo : dive.uuid; return ( } > Edit {onDelete && ( )} ), })} muted={!!addToTrip} backdrop={(coveredBottom) => { // The map's places and the water's bubbles centre above the profile // and the chips rather than behind them. const aboveProfile = outline ? coveredBottom + SILHOUETTE_GAP + SILHOUETTE_HEIGHT : chips ? coveredBottom + CHIPS_HEIGHT : coveredBottom; return ( <> } /> {/* Without the glow the details' text has: a `drop-shadow` over the fill's gradient draws it in bands. */} {outline && (
)} ); }} > {chips && ( // Over the foot of the backdrop's band rather than above the name in // the flow, so a dive with chips is as tall as one without. Lifted over // the card's link, so their hints are reachable, and only as wide as // they are - up to the card's width - so the link still takes a click // beside them. Not for a finger, whose tap on a card means the dive.
{chips}
)} {importRow?.open ? ( ) : linkTo ? ( {title} {addToTrip && ( {" (Not in this trip)"} )} ) : (
{title}
)}
); }