Ctrl K
ComunidadGuías…

src/lib/return-to.ts

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

Archivo original ↗Preguntar a KiBot
// Where a form's or a record page's "back" link, a form's Cancel button, and
// (where it makes sense) a save or a delete should send someone.
//
// The dive form is reachable from at least six places - the Home page, the dive
// list, the header's create menu, and the "log a dive here" buttons on a trip or
// a dive site - and a dive from a trip, a site, the Home page and more, so a
// hardcoded `/dives` is wrong for most of them. Rather than each caller
// threading its own href through the page, the destination is read off the URL,
// which means it also survives a reload or a shared link.
//
// Deliberately not `router.back()`: a deep link or a refresh has no history
// entry to pop, and after a successful save "back" points at the form that was
// just submitted.

import { sanitizeRedirectPath } from "@/lib/auth-redirect";

export interface ReturnTarget {
  href: string;
  label: string;
}

// Section labels for the back link, in both their list and single-record forms:
// `/trips` is "Back to trips", `/trips/{uuid}` is "Back to trip". Every page inside
// the chrome needs one, since the header's create menu opens the dive form from
// any of them - `app/page-frames.render.test.tsx` derives that set and checks it.
const SECTIONS: Record<string, { index: string; item: string }> = {
  home: { index: "home", item: "home" },
  dives: { index: "dives", item: "dive" },
  trips: { index: "trips", item: "trip" },
  sites: { index: "dive sites", item: "dive site" },
  gear: { index: "gear", item: "gear" },
  certifications: { index: "certifications", item: "certification" },
  courses: { index: "courses", item: "course" },
  contacts: { index: "contacts", item: "contact" },
  people: { index: "people", item: "person" },
  species: { index: "marine life", item: "species" },
  checkin: { index: "check-in", item: "check-in" },
  import: { index: "import", item: "import" },
  settings: { index: "settings", item: "settings" },
  admin: { index: "admin", item: "admin" },
  support: { index: "support", item: "support" },
  privacy: { index: "privacy policy", item: "privacy policy" },
  terms: { index: "terms of service", item: "terms of service" },
  developers: { index: "API reference", item: "API reference" },
};

export function labelForPath(path: string): string {
  const segments = path.split(/[?#]/)[0].split("/").filter(Boolean);
  if (segments.join("/") === "sites/proposals") return "Back to your proposals";
  const section = SECTIONS[segments[0]];
  if (!section) return "Back";
  return `Back to ${segments.length > 1 ? section.item : section.index}`;
}

// A form must never send someone back to a form. The header's create menu is
// present on the dive form itself, so without this a "New dive" opened from
// `/dives/new` would hand that same path back to Cancel.
export function isFormPath(path: string): boolean {
  const route = path.split(/[?#]/)[0].replace(/\/$/, "");
  return route === "/dives/new" || route.endsWith("/edit");
}

// Past this, a `from` keeps only its path. Every record followed from another
// nests the page before it, so a long enough walk would otherwise grow the URL
// without bound; this is a dozen or so records deep.
const FROM_MAX_LENGTH = 1000;

// `href` with a `?from=` naming the page it is followed from, so the page it
// opens can send the diver back there: a dive opened from a trip says "Back to
// trip". `from` is that page's path *and* query, so a trip opened from the
// Home page still says "Back to home" once the diver comes back to it from
// one of its dives.
//
// Nothing is added where the opened page's back link already points - its
// parent path, so a dive followed from `/dives` or an edit from its own dive
// keeps a clean URL - nor from a form, which nothing should return to. A link
// to the very page it is followed from is that page as it stands, way back and
// all, rather than a page that leads back to itself.
export function withReturnTo(href: string, from: string | null): string {
  if (!from || isFormPath(from)) return href;
  const path = href.split(/[?#]/)[0];
  if (from.split(/[?#]/)[0] === path) return from;
  if (from === path.slice(0, path.lastIndexOf("/"))) return href;
  const kept = from.length > FROM_MAX_LENGTH ? from.split(/[?#]/)[0] : from;
  const separator = href.includes("?") ? "&" : "?";
  return `${href}${separator}from=${encodeURIComponent(kept)}`;
}

export interface ReturnToParams {
  /** An explicit destination, set by whoever linked to the form. */
  from?: string | null;
  /** The form's own context params, used when there's no explicit `from`. */
  trip_uuid?: string | null;
  dive_site_uuid?: string | null;
  course_uuid?: string | null;
}

// Resolution order, most to least specific:
//   1. `?from=` - the linking page said exactly where to go back to.
//   2. The context the form was opened with. "Log a dive for this trip" already
//      passes `?trip_uuid=`, so that entry point needs no extra wiring to send
//      the diver back to the trip they were looking at.
//   3. The caller's own fallback (the dive list, or the dive being edited).
//
// `from` goes through the same `sanitizeRedirectPath` guard as the post-sign-in
// redirect: only same-origin, path-relative destinations are honoured, so a
// hand-crafted `?from=//evil.example` can't turn a Cancel button into an open
// redirect.
export function resolveReturnTarget(
  params: ReturnToParams,
  fallback: ReturnTarget,
): ReturnTarget {
  const from = sanitizeRedirectPath(params.from);
  if (from && !isFormPath(from)) {
    return { href: from, label: labelForPath(from) };
  }

  if (params.trip_uuid) {
    return {
      href: `/trips/${params.trip_uuid}`,
      label: "Back to trip",
    };
  }

  if (params.dive_site_uuid) {
    return {
      href: `/sites/${params.dive_site_uuid}`,
      label: "Back to dive site",
    };
  }

  if (params.course_uuid) {
    return {
      href: `/courses/${params.course_uuid}`,
      label: "Back to course",
    };
  }

  return fallback;
}