"use client"; import { useState, type ReactNode, type Ref } from "react"; import { CloudUpload, Plus } from "lucide-react"; import { DiveIcon } from "@/components/logo"; import { EmptyState } from "@/components/ui/empty-state"; import { ButtonLink } from "@/components/ui/button"; import { HERO_BODY, IndexHero } from "@/components/ui/map-hero"; import { cn } from "@/lib/utils"; import { Card, CardContent } from "@/components/ui/card"; import { CountBadge } from "@/components/ui/count-badge"; import { ListCardHeader, useIsEmptyList, } from "@/components/ui/list-card-header"; import { LoadMoreTrigger } from "@/components/ui/load-more-trigger"; import { ListFiltersButton } from "@/components/ui/list-filters-button"; import { DivesFilters, diveFiltersChanged, hasDiveFilters, NO_DIVE_FILTERS, type DiveListFilters, } from "@/components/dives/dives-filters"; import type { Tag } from "@/lib/api/tags"; import { BackdropCardList } from "@/components/ui/backdrop-card"; export interface DivesPageFrameProps { isLoading: boolean; totalCount: number; /** Whether the query that answered `totalCount` narrowed the list. */ isCountNarrowed?: boolean; itemsPerPage: number; /** One `DiveCard` per dive - list items, for the list this frame draws. */ cards?: ReactNode[]; /** * The numbering card above the list card. It draws nothing until its own * request lands, and nothing at all for a log already numbered consecutively * in date order, so leaving it out is the page's own first render. */ numbering?: ReactNode; /** The drafts card, above the numbering. It too draws nothing while there are none. */ drafts?: ReactNode; /** * Attached to the page's `h1`, which is `tabIndex={-1}` so it can be given * focus when something between it and the list removes itself. */ headingRef?: Ref; isLoadingMore?: boolean; loadFailed?: boolean; hasMore?: boolean; onLoadMore?: () => void; /** The tag, the type and the order the list is read in. */ filters?: DiveListFilters; onFiltersChange?: (filters: DiveListFilters) => void; /** * Fired each time the filter panel is opened. The page reads the diver's tags * off the back of it - the panel is shut on arrival, so most visits need no * such request at all. */ onFiltersOpened?: () => void; /** What the tag select offers. */ tags?: readonly Tag[]; } const noop = () => {}; // Everything /dives draws before its cards exist, kept apart from the data render so // the page's first render is this frame. Every data-varying prop is optional, // and the defaults are that first render. export function DivesPageFrame({ isLoading, totalCount, isCountNarrowed = false, itemsPerPage, cards = [], numbering, drafts, headingRef, isLoadingMore = false, loadFailed = false, hasMore = false, onLoadMore = noop, filters = NO_DIVE_FILTERS, onFiltersChange = noop, onFiltersOpened = noop, tags, }: DivesPageFrameProps) { const [isPanelOpen, setPanelOpen] = useState(false); const isNarrowed = hasDiveFilters(filters); // An unstarted logbook, rather than one filtered down to nothing - see // `useIsEmptyList`. const isEmptyList = useIsEmptyList({ isLoading, count: cards.length, isNarrowed, }); // Spaced by the container rather than by a margin on each block: the // numbering card is absent more often than not, and a gap it carried itself // would be left behind on the pages where it draws nothing. return (
Import Log new dive } />
{drafts} {numbering} { if (open) onFiltersOpened(); setPanelOpen(open); }} onClear={() => onFiltersChange(NO_DIVE_FILTERS)} isChanged={diveFiltersChanged(filters)} label="Filter and sort dives" /> {/* Hidden rather than unmounted, so `aria-controls` points at something, and gone with the button that opens it for a list with nothing in it to narrow. */} {!isEmptyList && ( )} {/* The card is the list's header - its count and its filters - and what it says when there is nothing to list. The dives themselves are cards of their own, so they sit below it rather than in it. */} {!isLoading && cards.length === 0 && ( {isNarrowed ? ( // A list filtered to nothing is not an empty logbook, so it // keeps one line and no "log your first dive" - see "One // `EmptyState`, and the filtered list is not one" in // DECISIONS.md.
No dives match those filters.
) : ( Log your first dive } /> )}
)}
{(isLoading || cards.length > 0) && ( // One dive to a row below `lg`, two above, as /trips: a card may hold // a map, and a browser keeps only so many of those per page - see // `BackdropCard`. {cards} )}
); }