Ctrl K
ComunidadGuías…

src/components/dives/dive-numbering-card.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { useCallback, useEffect, useState } from "react";
import { ListOrdered, ListRestart } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { divesAPI, DiveNumberingSummary } from "@/lib/api/dives";
import { describeDiveNumbering } from "@/lib/dive-numbering";
import { RenumberDivesDialog } from "@/components/dives/renumber-dives-dialog";

export interface DiveNumberingCardProps {
  // False until there's a signed-in user - the endpoint reads the caller's own
  // log and takes no user uuid.
  enabled: boolean;
  // Bumped by the page whenever the log itself changes (a dive deleted, say),
  // since that moves numbers this card has already described.
  reloadToken?: number;
  // Called after a renumber writes, so the page can refetch the dive list.
  onRenumbered: () => void;
  // Called when a renumber leaves the log with nothing to renumber, just
  // before this card goes. The page owes focus somewhere that survives.
  onVanished?: () => void;
}

// A card above the dive list saying what a renumber would tidy, and the way in
// to doing it.
//
// Muted and factual: see `describeDiveNumbering` for why this never phrases a
// gap as a problem, and why it says nothing at all about a log already
// numbered consecutively in date order - which is what keeps this card off
// most pages most of the time. Its own card rather than a line inside the
// list's, because it is about the log rather than part of it, and it comes and
// goes.
//
// Renumber sits here rather than in settings because this is where a diver is
// when the question occurs to them.
export function DiveNumberingCard({
  enabled,
  reloadToken,
  onRenumbered,
  onVanished,
}: DiveNumberingCardProps) {
  const [summary, setSummary] = useState<DiveNumberingSummary | null>(null);
  const [isDialogOpen, setIsDialogOpen] = useState(false);

  // Returns what it read as well as storing it, so the renumber path below can
  // tell whether this card is about to remove itself.
  const load = useCallback(async () => {
    try {
      const next = await divesAPI.getDiveNumbering();
      setSummary(next);
      return next;
    } catch (error) {
      // Silent, and the card simply doesn't render. This is a description of
      // the log, not part of it - nothing here is worth a toast over the dive
      // list the diver actually came for.
      console.error("Failed to load dive numbering:", error);
      setSummary(null);
      return null;
    }
  }, []);

  useEffect(() => {
    // Deliberate fetch-on-mount pattern (the state update happens after the
    // network await), suppressed the same way as in `useInfiniteResource` -
    // see the note there on this rule's false positive.
    if (enabled) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      load();
    }
  }, [enabled, reloadToken, load]);

  const description = summary ? describeDiveNumbering(summary) : null;
  if (description === null) return null;

  return (
    <>
      {/* `pt-(--card-pad)` because `CardContent`'s own padding assumes a header sits above
          it, and a "Numbering" title would only restate the sentence under it. */}
      <Card>
        <CardContent className="pt-(--card-pad)">
          <div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground">
            <span className="flex items-center gap-2">
              <ListOrdered className="h-4 w-4 shrink-0" />
              {description}
            </span>
            {/* Matches the "New set" button on `/gear` - the same outline/sm
                shape every card-level action in the app uses. `ListRestart` for
                the action, distinct from the `ListOrdered` labelling the
                sentence itself. */}
            <Button
              variant="outline"
              size="sm"
              onClick={() => setIsDialogOpen(true)}
            >
              <ListRestart className="h-4 w-4 mr-2" />
              Renumber
            </Button>
          </div>
        </CardContent>
      </Card>

      <RenumberDivesDialog
        open={isDialogOpen}
        onOpenChange={setIsDialogOpen}
        onRenumbered={() => {
          // The dialog hands focus back to the Renumber button as it closes,
          // and a renumber that tidies the log takes that button off the page
          // a moment later - dropping focus to `<body>`, where nothing says
          // what happened. `onVanished` runs on the summary that decided it,
          // before the render that removes the button, so the page can move
          // focus while there is still focus to move.
          void load().then((next) => {
            if (next && describeDiveNumbering(next) === null) onVanished?.();
          });
          onRenumbered();
        }}
      />
    </>
  );
}