"use client"; import { useCallback, useEffect, useRef } from "react"; import { divesAPI, Dive } from "@/lib/api/dives"; import { useInfiniteResource } from "@/hooks/useInfiniteResource"; import { useWithReturnTo } from "@/hooks/useReturnTo"; import { CARD_TITLE_ACTION, CARD_TITLE_ROW, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { ButtonLink } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { LoadMoreTrigger } from "@/components/ui/load-more-trigger"; import { BackdropCardList } from "@/components/ui/backdrop-card"; import { DiveCard } from "@/components/dives/dive-card"; import { Plus } from "lucide-react"; import { DiveIcon } from "@/components/logo"; import { cn } from "@/lib/utils"; const RECENT_DIVES_COUNT = 5; // The page size for the scoped lists on the detail pages. Ten, like the other // list pages, rather than the API's 100-row ceiling: the rows are links a reader // scans, and a first paint of ten arrives sooner than one of a hundred. const DIVES_PER_PAGE = 10; export interface RecentDivesCardProps { // False until there's a signed-in user - the list reads the caller's own log // and takes no user uuid, so the page has to say when the session is known. enabled: boolean; // Only show dives made at this dive site. When omitted, shows dives // regardless of dive site. diveSiteId?: string; // Only show dives this gear item was used on. When omitted, shows dives // regardless of gear. gearItemId?: string; // Only show dives that were part of this training course. When omitted, shows // dives regardless of course. courseId?: string; // Only show dives that recorded this species. When omitted, shows dives // regardless of what was spotted. speciesId?: string; // Only show dives that name this person. When omitted, shows dives regardless // of who was on them. personId?: string; // Show every dive in scope, a page at a time as the reader scrolls, rather // than Home's fixed preview of the latest few. // // A flag rather than a large `limit`: the API clamps `items_per_page` to 100, // so a diver past that number would be shown a list that looks complete and // is not. // // A complete list that holds nothing also drops its header down to its hidden // heading, as the list pages' cards do: the empty state already says what the // card is for. The Home page's preview keeps it beside the trips card. complete?: boolean; // Re-read the dives on screen, in place, whenever this changes - after an // edit to the record they are scoped to that shows on their cards: a site's // name is in their titles and its pin is on their maps. refreshOn?: unknown; title?: string; description?: string; // Href/label for the header's "view all" button. Pass `null` to hide it // entirely (e.g. when the card already shows the full list). viewAllHref?: string | null; viewAllLabel?: string; emptyTitle?: string; emptyDescription?: string; newDiveHref?: string; newDiveLabel?: string; } // Shows a list of dives for a user, each as a `DiveCard`. // Used on the Home page (the most recent few) and on the detail pages that // scope dives to one record - a dive site, a gear item, a course, a species, a // person - so they all stay in sync. export function RecentDivesCard({ enabled, diveSiteId, gearItemId, courseId, speciesId, personId, complete = false, refreshOn, title = "Recent Dives", description, viewAllHref = "/dives", viewAllLabel = "View all dives", emptyTitle = "No dives logged yet", emptyDescription = "Start your diving journey by logging your first dive!", newDiveHref = "/dives/new", newDiveLabel = "Log your first dive", }: RecentDivesCardProps) { const withReturnTo = useWithReturnTo(); const fetchDives = useCallback( (page: number, perPage: number) => divesAPI.getDives(page, perPage, { diveSiteUuid: diveSiteId, gearItemUuid: gearItemId, courseUuid: courseId, speciesUuid: speciesId, personUuid: personId, }), [diveSiteId, gearItemId, courseId, speciesId, personId], ); // The preview asks for its few rows once and stops; a complete list pages // through in tens. `hasMore` is forced false for the preview so the trigger // below renders nothing - the header's "View all dives" button is where that // card's "more" lives, and offering both would be two answers to one question. const { items: recentDives, isLoading: isLoadingDives, isLoadingMore, totalCount, itemsPerPage, hasMore, loadFailed, loadMore, revalidate, } = useInfiniteResource(fetchDives, { keyOf: (dive) => dive.uuid, enabled, itemsPerPage: complete ? DIVES_PER_PAGE : RECENT_DIVES_COUNT, errorMessage: "Failed to load dives. Please try again.", }); // Against the value last refreshed for rather than on every run, so the first // render and a hidden route shown again read nothing extra. const refreshedFor = useRef(refreshOn); useEffect(() => { if (Object.is(refreshedFor.current, refreshOn)) return; refreshedFor.current = refreshOn; void revalidate(); }, [refreshOn, revalidate]); const isEmpty = !isLoadingDives && recentDives.length === 0; const isHeaderless = complete && isEmpty; return ( {isHeaderless ? ( {title} ) : ( <> {/* A detail page's card has no link, and sits as high as its neighbours. */}
{title} {viewAllHref && ( {viewAllLabel} )}
{description && {description}} )}
{isLoadingDives ? ( // `RECENT_DIVES_COUNT` either way: on the Home page it is exactly // the preview's size, and on a detail page the real count isn't // knowable up front, where a few rows is a better guess than a // screen of them. ) : isEmpty ? ( {newDiveLabel} } /> ) : ( {recentDives.map((dive) => ( ))} )}
); }