Ctrl K
ComunidadGuías…

src/components/dives/edit-dive-page-content.tsx

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

Archivo original ↗Preguntar a KiBot
"use client";

import { useState, useCallback } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useAuthGuard } from "@/hooks/useAuthGuard";
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
import { useResource } from "@/hooks/useResource";
import { useReturnTo } from "@/hooks/useReturnTo";
import { withReturnTo } from "@/lib/return-to";
import { divesAPI, Dive } from "@/lib/api/dives";
import {
  buildDiveUpdate,
  diveToFormValues,
  diveUpdateSchema,
  DiveUpdateInput,
  emptyDiveFormValues,
} from "@/lib/validations/dive";
import { useMixtureFieldArray } from "@/components/dives/mixture-fields";
import { usePendingDiveFiles } from "@/components/dives/use-pending-dive-files";
import { useDiveFormVisibility } from "@/hooks/useDiveFormVisibility";
import { useDiveConditionsFill } from "@/hooks/useDiveConditionsFill";
import { entryFixAfterSave } from "@/lib/dive-recordings";
import { DiveForm } from "@/components/dives/dive-form";
import { fetchedMarks } from "@/lib/dive-conditions";
import { DiveFormFieldsMenu } from "@/components/dives/dive-form-fields-menu";
import { DiveIcon } from "@/components/logo";
import {
  FORM_BODY,
  FORM_COLUMN,
  PAGE_COLUMN,
  PlainHero,
} from "@/components/ui/map-hero";
import { PageSpinner } from "@/components/ui/page-spinner";
import { FormPageSkeleton } from "@/components/ui/page-skeleton";
import { NotFoundState } from "@/components/ui/not-found-state";
import { useToast } from "@/components/ui/use-toast";
import { getApiErrorMessage } from "@/lib/api/error";

export function EditDivePageContent() {
  const { user, isAuthenticated, isLoading: isAuthLoading } = useAuthGuard();
  const router = useRouter();
  const { toast } = useToast();
  const [isSubmitting, setIsSubmitting] = useState(false);

  const form = useForm<DiveUpdateInput>({
    resolver: zodResolver(diveUpdateSchema),
    defaultValues: { ...emptyDiveFormValues(), start_time: "" },
  });
  const mixtureFieldArray = useMixtureFieldArray(form.control);
  // `fillsDefaults: false` - the edit form has no defaults to carry. What appears
  // when a field is shown is the stored value, and hiding it never changes form
  // state, which is react-hook-form's `shouldUnregister: false` doing all of the
  // work by itself.
  const visibility = useDiveFormVisibility({
    form,
    replaceMixtures: mixtureFieldArray.replace,
    fillsDefaults: false,
  });
  const { reveal, revealNonEmpty } = visibility;

  // Seeds the form from the loaded dive, and is one of the moments a value
  // arrives from outside the diver's typing: a dive that records notes shows
  // its notes even under a preset that hides them, because an edit form quietly
  // holding data the diver cannot see is the one thing this feature must not do.
  const resetFromDive = useCallback(
    (diveData: Dive) => {
      const values = diveToFormValues(diveData);
      form.reset(values);
      revealNonEmpty(values);
      // The import's section holds the dive's files, which can be struck off there.
      if ((diveData.recordings ?? []).length > 0) reveal(["file_import"]);
    },
    [form, reveal, revealNonEmpty],
  );

  const {
    id: diveId,
    resource: dive,
    isLoading: isLoadingDive,
  } = useResource<Dive>(divesAPI.getDive, {
    enabled: !!user,
    errorMessage: "Failed to load dive details. Please try again.",
    redirectTo: "/dives",
    onLoaded: resetFromDive,
  });
  const files = usePendingDiveFiles(dive?.recordings);
  // At the entry fix the save leaves, with the files struck off and picked here.
  const savedPosition = entryFixAfterSave({
    recordings: dive?.recordings,
    removed: files.fileList.removedStored,
    pending: files.fileList.pending,
    stored:
      dive?.entry_latitude != null && dive.entry_longitude != null
        ? { latitude: dive.entry_latitude, longitude: dive.entry_longitude }
        : null,
  });
  const conditions = useDiveConditionsFill({
    form,
    visibility,
    enabled: !!dive,
    stored: dive ? fetchedMarks(dive) : undefined,
    fetchedOnOpen: dive?.conditions != null,
    position: savedPosition,
  });
  useDocumentTitle(
    dive ? `Edit Dive #${dive.dive_number}` : undefined,
    "Dives",
  );

  // Back/Cancel return to wherever the edit was started from - the dive list, a
  // trip, an explicit `?from=` - falling back to the dive itself.
  const returnTo = useReturnTo({
    href: `/dives/${diveId}`,
    label: "Back to dive",
  });

  const onSubmit = async (data: DiveUpdateInput) => {
    if (!user || !diveId) return;

    try {
      setIsSubmitting(true);

      const updateData = buildDiveUpdate(conditions.withhold(data));

      await divesAPI.updateDive(diveId, updateData);

      await files.save(diveId, { saved: "Dive updated, but " });

      toast({
        title: "Success",
        description: "Dive updated successfully!",
      });

      // The dive, with its back link going where the form's would have.
      router.push(withReturnTo(`/dives/${diveId}`, returnTo.href));
    } catch (error) {
      console.error("Failed to update dive:", error);

      const errorMessage = getApiErrorMessage(
        error,
        "Failed to update dive. Please try again.",
      );

      toast({
        title: "Error",
        description: errorMessage,
        variant: "destructive",
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  if (isAuthLoading) {
    return <PageSpinner variant="inset" />;
  }

  if (!isAuthenticated) {
    return null; // Will redirect to signin
  }

  if (isLoadingDive) {
    return (
      <FormPageSkeleton
        backHref={returnTo.href}
        backLabel={returnTo.label}
        // The dive form's first card is deeper than the six-field default -
        // date, dive time, depths, temperature, site and trip all land above
        // the fold, and undersizing the placeholder puts the jump back.
        fields={8}
      />
    );
  }

  if (!dive) {
    return (
      <div className={PAGE_COLUMN}>
        <NotFoundState
          message="Dive not found."
          backHref="/dives"
          backLabel="Back to dives"
        />
      </div>
    );
  }

  return (
    <div>
      <PlainHero
        backHref={returnTo.href}
        backLabel={returnTo.label}
        icon={DiveIcon}
        title={`Edit Dive #${dive.dive_number}`}
        subtitle="Update the details of your dive"
        figures={[]}
        column={FORM_COLUMN}
        actions={<DiveFormFieldsMenu visibility={visibility} />}
      />

      <div className={FORM_BODY}>
        <DiveForm
          form={form}
          mixtureFieldArray={mixtureFieldArray}
          visibility={visibility}
          mode="edit"
          onSubmit={onSubmit}
          isSubmitting={isSubmitting}
          cancelHref={returnTo.href}
          submittingLabel="Saving..."
          submitLabel="Save changes"
          fileList={files.fileList}
          recordings={dive.recordings ?? []}
          diveUuid={dive.uuid}
          // The dive already carries its sites' names, so the picker doesn't have
          // to look them up again just to label the rows it starts out with.
          knownDiveSites={dive.dive_sites}
          diveFix={savedPosition}
          knownGearItems={dive.gear_items}
          knownSpecies={dive.sightings}
          fetched={conditions.marks}
        />
      </div>
    </div>
  );
}