refactor(web): add BackLink component, use history.back with fallback

Quentin Gliech and Claude Opus 4.6 (1M context) created

create a reusable BackLink component that:
- uses router.history.back() when canGoBack is true (preserves
  scroll position, filter state, pagination)
- falls back to a typed Link destination when there's no history
  (e.g. direct navigation to a detail page)

replace all "Back to issues" Links and the CommitPage back button
with BackLink across bug detail, new issue, user profile, and
commit pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/src/components/ui/back-link.tsx         | 37 ++++++++++++++++++++
webui2/src/routes/$repo/_issues/issues/$id.tsx | 12 +----
webui2/src/routes/$repo/_issues/issues/new.tsx | 14 ++-----
webui2/src/routes/$repo/_issues/user/$id.tsx   | 12 +----
webui2/src/routes/$repo/commit/$hash.tsx       | 19 +++-------
5 files changed, 53 insertions(+), 41 deletions(-)

Detailed changes

webui2/src/components/ui/back-link.tsx 🔗

@@ -0,0 +1,37 @@
+import { Link, useCanGoBack, useRouter } from "@tanstack/react-router";
+import type { LinkProps } from "@tanstack/react-router";
+import { ArrowLeft } from "lucide-react";
+
+// A "Back to ..." link that uses browser history when available,
+// falling back to a typed Link destination otherwise.
+// This preserves scroll position and filter state when navigating
+// back from a detail page to a list.
+export function BackLink({
+  children,
+  ...fallbackProps
+}: LinkProps & { children: React.ReactNode }) {
+  const canGoBack = useCanGoBack();
+  const router = useRouter();
+
+  if (canGoBack) {
+    return (
+      <button
+        onClick={() => router.history.back()}
+        className="text-muted-foreground hover:text-foreground mb-4 flex items-center gap-1.5 text-sm"
+      >
+        <ArrowLeft className="size-3.5" />
+        {children}
+      </button>
+    );
+  }
+
+  return (
+    <Link
+      {...fallbackProps}
+      className="text-muted-foreground hover:text-foreground mb-4 flex items-center gap-1.5 text-sm"
+    >
+      <ArrowLeft className="size-3.5" />
+      {children}
+    </Link>
+  );
+}

webui2/src/routes/$repo/_issues/issues/$id.tsx 🔗

@@ -1,7 +1,6 @@
 import { useReadQuery } from "@apollo/client/react";
 import { createFileRoute, Link } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
-import { ArrowLeft } from "lucide-react";
 
 import { type BugDetailQuery, BugDetailDocument } from "@/__generated__/graphql";
 import { CommentBox } from "@/components/bugs/CommentBox";
@@ -10,6 +9,7 @@ import { StatusBadge } from "@/components/bugs/StatusBadge";
 import { Timeline } from "@/components/bugs/Timeline";
 import { TitleEditor } from "@/components/bugs/TitleEditor";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { BackLink } from "@/components/ui/back-link";
 import { Separator } from "@/components/ui/separator";
 import { Skeleton } from "@/components/ui/skeleton";
 
@@ -42,15 +42,9 @@ function RouteComponent() {
 
   return (
     <div>
-      <Link
-        to="/$repo/issues"
-        params={{ repo: repo }}
-        search={{ q: "status:open", after: "" }}
-        className="text-muted-foreground hover:text-foreground mb-4 flex items-center gap-1.5 text-sm"
-      >
-        <ArrowLeft className="size-3.5" />
+      <BackLink to="/$repo/issues" params={{ repo }} search={{ q: "status:open", after: "" }}>
         Back to issues
-      </Link>
+      </BackLink>
 
       {/* Title row — hover reveals edit button when logged in */}
       <div className="mb-3">

webui2/src/routes/$repo/_issues/issues/new.tsx 🔗

@@ -1,9 +1,9 @@
-import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
-import { ArrowLeft } from "lucide-react";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
 import { useState } from "react";
 
 import { useBugCreateMutation } from "@/__generated__/graphql";
 import { Markdown } from "@/components/content/Markdown";
+import { BackLink } from "@/components/ui/back-link";
 import { Button } from "@/components/ui/button";
 import { ButtonLink } from "@/components/ui/button-link";
 import { Input } from "@/components/ui/input";
@@ -38,15 +38,9 @@ function RouteComponent() {
 
   return (
     <div className="mx-auto max-w-3xl">
-      <Link
-        to="/$repo/issues"
-        params={{ repo: repo }}
-        search={{ q: "status:open", after: "" }}
-        className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
-      >
-        <ArrowLeft className="size-3.5" />
+      <BackLink to="/$repo/issues" params={{ repo }} search={{ q: "status:open", after: "" }}>
         Back to issues
-      </Link>
+      </BackLink>
 
       <h1 className="mb-6 text-xl font-semibold">New issue</h1>
 

webui2/src/routes/$repo/_issues/user/$id.tsx 🔗

@@ -9,7 +9,6 @@
 import { createFileRoute, Link } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
 import {
-  ArrowLeft,
   MessageSquare,
   CircleDot,
   CircleCheck,
@@ -22,6 +21,7 @@ import { useState } from "react";
 import { Status, useUserProfileQuery } from "@/__generated__/graphql";
 import { LabelBadge } from "@/components/bugs/LabelBadge";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { BackLink } from "@/components/ui/back-link";
 import { Button } from "@/components/ui/button";
 import { Skeleton } from "@/components/ui/skeleton";
 import { cn } from "@/lib/utils";
@@ -96,15 +96,9 @@ function RouteComponent() {
 
   return (
     <div>
-      <Link
-        to="/$repo/issues"
-        params={{ repo: repo }}
-        search={{ q: "status:open", after: "" }}
-        className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
-      >
-        <ArrowLeft className="size-3.5" />
+      <BackLink to="/$repo/issues" params={{ repo }} search={{ q: "status:open", after: "" }}>
         Back to issues
-      </Link>
+      </BackLink>
 
       {/* ── Profile header ─────────────────────────────────────────────── */}
       <div className="mb-8 flex items-start gap-5">

webui2/src/routes/$repo/commit/$hash.tsx 🔗

@@ -3,11 +3,12 @@
 
 import { gql } from "@apollo/client";
 import { useReadQuery } from "@apollo/client/react";
-import { createFileRoute, Link, useCanGoBack, useRouter } from "@tanstack/react-router";
+import { createFileRoute, Link } from "@tanstack/react-router";
 import { format } from "date-fns";
-import { ArrowLeft, GitCommit } from "lucide-react";
+import { GitCommit } from "lucide-react";
 
 import { FileDiffView } from "@/components/code/FileDiffView";
+import { BackLink } from "@/components/ui/back-link";
 import { Skeleton } from "@/components/ui/skeleton";
 
 const COMMIT_QUERY = gql`
@@ -68,8 +69,6 @@ function RouteComponent() {
   const { repo } = Route.useParams();
   const { commitRef } = Route.useLoaderData();
   const { data } = useReadQuery(commitRef);
-  const canGoBack = useCanGoBack();
-  const router = useRouter();
 
   const commit = data?.repository?.commit;
   if (!commit) return null;
@@ -79,15 +78,9 @@ function RouteComponent() {
 
   return (
     <div>
-      {canGoBack && (
-        <button
-          onClick={() => router.history.back()}
-          className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
-        >
-          <ArrowLeft className="size-3.5" />
-          Back
-        </button>
-      )}
+      <BackLink to="/$repo" params={{ repo }}>
+        Back
+      </BackLink>
 
       <div className="border-border mb-6 rounded-md border p-5">
         <div className="mb-1 flex items-start gap-3">