"use client"; import { useCallback, useEffect, useState } from "react"; import { FilePen, Trash2 } from "lucide-react"; import { Button, ButtonLink } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useToast } from "@/components/ui/use-toast"; import { diveDraftsAPI, type DiveDraft } from "@/lib/api/dive-drafts"; import { diveSitesAPI } from "@/lib/api/dive-sites"; import { getApiErrorMessage, getApiErrorStatus } from "@/lib/api/error"; import { formatDateTime, formatDiveStartTime } from "@/lib/date-time"; // The API's ceiling on a page. A diver keeps a handful of drafts at most, so one // page is the whole list in practice. const DRAFTS_PAGE_SIZE = 100; export interface DiveDraftsCardProps { // False until there's a signed-in user. enabled: boolean; // Called when the last draft is discarded, just before this card goes - the // page owes focus somewhere that survives. onVanished?: () => void; } /** The form a draft opens in. */ export function continueDraftHref(draftUuid: string): string { return `/dives/new?draft=${encodeURIComponent(draftUuid)}`; } // The account's dive drafts, above the dive list: the dives started on this // browser or on the OpenDiving app and not yet logged. Nothing at all while there // are none, like the numbering card - it comes and goes. export function DiveDraftsCard({ enabled, onVanished }: DiveDraftsCardProps) { const { toast } = useToast(); const [drafts, setDrafts] = useState([]); // The primary site's name per site uuid, from one lookup over the drafts shown. const [siteNames, setSiteNames] = useState>({}); const [pendingUuid, setPendingUuid] = useState(null); const [isDiscarding, setIsDiscarding] = useState(false); const load = useCallback(async () => { try { const page = await diveDraftsAPI.getDrafts(1, DRAFTS_PAGE_SIZE); setDrafts(page.data); const primaries = [ ...new Set( page.data .map((draft) => draft.values.dive_site_uuids?.[0]) .filter((uuid): uuid is string => !!uuid), ), ]; if (primaries.length === 0) return; try { const sites = await diveSitesAPI.lookupDiveSitesByUuid(primaries); setSiteNames( Object.fromEntries(sites.map((site) => [site.uuid, site.name])), ); } catch (error) { // The rows still list without their site names. console.error("Failed to look up the drafts' dive sites:", error); } } catch (error) { // Silent, and the card doesn't render: the dive list is what the diver // came for, and a toast over it about drafts would be noise. console.error("Failed to load dive drafts:", error); } }, []); useEffect(() => { // Fetch-on-mount, suppressed as in `DiveNumberingCard`. if (enabled) { // eslint-disable-next-line react-hooks/set-state-in-effect load(); } }, [enabled, load]); const confirmDiscard = async () => { if (!pendingUuid) return; setIsDiscarding(true); const drop = () => { const remaining = drafts.filter((draft) => draft.uuid !== pendingUuid); if (remaining.length === 0) onVanished?.(); setDrafts(remaining); setPendingUuid(null); }; try { await diveDraftsAPI.deleteDraft(pendingUuid); drop(); } catch (error) { // Logged or discarded on another device meanwhile: gone, as asked. if (getApiErrorStatus(error) === 404) { drop(); return; } toast({ title: "Error", description: getApiErrorMessage( error, "Failed to discard the draft. Please try again.", ), variant: "destructive", }); } finally { setIsDiscarding(false); } }; if (drafts.length === 0) return null; return ( <> Drafts
    {drafts.map((draft) => { const start = draft.values.start_time; const site = draft.values.dive_site_uuids?.[0]; const siteName = site ? siteNames[site] : undefined; return (
  • {start ? formatDiveStartTime(start) : "Untitled draft"}
    {siteName ? `${siteName} ยท ` : ""} Changed {formatDateTime(draft.updated_at)}
    Continue
  • ); })}
!open && setPendingUuid(null)} title="Discard draft" description={DISCARD_DRAFT_CONFIRMATION} confirmText="Discard" isLoading={isDiscarding} onConfirm={confirmDiscard} /> ); } /** What both Discard buttons ask before a draft is deleted. */ export const DISCARD_DRAFT_CONFIRMATION = "This deletes the draft on every device. It cannot be undone.";